diff --git a/docs/dream3d_nx_release_dates.md b/docs/dream3d_nx_release_dates.md index 8ff4786115..3895f56758 100644 --- a/docs/dream3d_nx_release_dates.md +++ b/docs/dream3d_nx_release_dates.md @@ -2,7 +2,7 @@ | Release Version | Release Date | |-----------------|--------------| -| 7.4.2 | 2026-08-31 | +| 7.4.2 | 2026-08-28 | | 7.4.1 | 2026-03-23 | | 7.4.0 | 2025-10-27 | | 7.2.2 | 2025-09-28 | diff --git a/docs/vv_templates/commit_template.md b/docs/vv_templates/commit_template.md index cd96afe4e0..0a66fe89f7 100644 --- a/docs/vv_templates/commit_template.md +++ b/docs/vv_templates/commit_template.md @@ -9,7 +9,7 @@ VV: fully V&V'ed Summary: - Found and fixed bug(s) (); -- documented deviation(s) from DREAM3D 6.5.171 (); +- documented deviation(s) from DREAM.3D v6.5.171 (); - retired

tests (); - unit tests replaced with inlined ** test fixtures; - added 3 V&V source-tree deliverables (report, deviations, provenance); @@ -40,7 +40,7 @@ VV: Compute Feature Neighbor Misorientations fully V&V'ed Summary: - Found and fixed 1 bug (divisor clobbered inside inner j-loop of algorithm.cpp); -- documented 2 deviations from DREAM3D 6.5.171 (D1 divisor bug, D2 EbsdLib 2.4.1 precision); +- documented 2 deviations from DREAM.3D v6.5.171 (D1 divisor bug, D2 EbsdLib 2.4.1 precision); - retired 2 tests (circular-oracle exemplar consumer + UNIMPLEMENTED stub); - unit tests replaced with 4 inlined *Class 1 (Analytical) + Class 4 (Invariant)* test fixtures; - added 3 V&V source-tree deliverables (report, deviations, provenance); @@ -52,7 +52,7 @@ Summary: | Situation | Bullet phrasing | |----------------------------------------------------|-----------------------------------------------------------------------| | Clean Port, no bugs found | `Confirmed no bugs (clean Port);` | -| No deviations from legacy | `Confirmed no deviations from DREAM3D 6.5.171;` | +| No deviations from legacy | `Confirmed no deviations from DREAM.3D v6.5.171;` | | Fresh V&V, no prior tests to retire | `No prior tests retired (fresh V&V cycle);` | | Existing tests kept, only new ones added | `Augmented existing tests with inlined …;` | | Doc was already correct | *omit the last bullet entirely (it's the only optional one)* | diff --git a/docs/vv_templates/defect_hunt.md b/docs/vv_templates/defect_hunt.md new file mode 100644 index 0000000000..1298142411 --- /dev/null +++ b/docs/vv_templates/defect_hunt.md @@ -0,0 +1,97 @@ +# Defect Hunt — Finding Bugs, Not Only Documenting Behavior + +The other V&V phases show that a filter agrees with an oracle on the fixtures that the engineer chose. They do not show that the engineer chose fixtures that can find bugs. The engineer designs the oracle after reading the code, so the fixtures tend to exercise the cases that the code already handles. The defect hunt is an adversarial pass with one goal: find an input that makes the filter produce a wrong result. + +**Motivating example — issue #1758.** In `AlignGeometries`, the Centroid branch computes `{t[0]-m[0], t[0]-m[0], t[0]-m[0]}`, so the X offset is applied on all three axes. The Origin branch next to it uses the correct indices. The tests run only the Origin mode. A fixture with the same offset on every axis also passes. Each of parts 1–4 below finds this defect independently. + +## When + +Do the defect hunt after `vv-discover` has listed the code paths and before the oracle fixtures are final. Its findings change the fixture design. Do the hunt again when a rebase or a fix changes the algorithm. + +The defect hunt does not replace the second-engineer review of the oracle. It is the author's own attempt to make the filter fail. + +## Part 1 — Bug-pattern review + +Read every line of the algorithm, its helpers, and the filter's `preflightImpl()`/`executeImpl()`. For each pattern, look for a match. Then do a separate pass in which you compare sibling code side by side. + +| Pattern | What to look for | +|---|---| +| **Copy-paste drift** | Per-axis or per-component expressions in which the index does not advance (`[0],[0],[0]`, `x, x, z`). Sibling branches (mode A vs. mode B, X vs. Y, in-core vs. OOC) that compute the same quantity in different ways. Put sibling branches side by side and compare them token by token. | +| **Index and stride** | X/Y/Z order. Tuple index vs. component index. Strides (`dims[0]*dims[1]`). An internal iteration order that differs from the physical order the user sees (for example, a slice loop that runs from the top down, then is indexed by a user "slice k"). | +| **Bounds and off-by-one** | `>` vs. `>=`. A loop that starts at 1 and skips element 0 or the anchor. The last slice, row, or feature. Empty and single-element inputs. | +| **Numerics** | Truncation where the code must round. Signed/unsigned wrap. Narrowing casts. Division by zero. A NaN cast to an integer (undefined behavior). Accumulation in `float` where the code needs `double`. | +| **Uninitialized or stale state** | Output tuple 0. Buffers that the code reuses between features or slices and does not reset. Accumulators declared outside the loop. | +| **Wrong object** | The wrong array, variable, or path among similar names. A parameter that the code reads but does not use. A result written to the wrong output. | +| **Discarded results** | A `Result<>` or warning that the code ignores. An error code that two different failures use. | +| **Mode gating** | A parameter that has no effect in a mode where it must have an effect. A mode branch that no test executes. | +| **Component shape not declared** | The algorithm reads an input DataArray with a fixed number of components (`a[2*i+1]`, `a[3*i+k]`, `getComponent(i, 2)`), but the `ArraySelectionParameter` or `MultiArraySelectionParameter` that selects it declares no `AllowedComponentShapes`, or declares a different one. The fix belongs in the parameter (`AllowedComponentShapes{{N}}`), because parameter validation then rejects other shapes (−208) before preflight runs. When N depends on another parameter or has variable dimensions, preflight must check `getComponentShape()` instead. Example: WriteStlFile's phases parameter declared `{{1}}` while the algorithm read 2 components. | +| **Parent Attribute Matrix not checked** | An element-level input DataArray (one value per cell, vertex, edge or face) is not checked to be a child of the geometry's element Attribute Matrix. A selection parameter accepts a DataPath into **any** Attribute Matrix, so a DataArray with the wrong number of tuples is a reachable input, not corrupt data. The fix is a preflight check with `IsChildOfAttributeMatrix` against `getCellData()` (Image or Rectilinear Grid Geometry) or the Vertex, Edge, Face or Polyhedra Attribute Matrix of a node geometry. `AttributeMatrix` only accepts children whose tuple count matches its own. Use the accessors that return a pointer; the `*Ref()` accessors and `getCellDataPath()` throw when the Attribute Matrix is missing. | +| **Feature-level bound** | A Feature Attribute Matrix has largest Feature Id + 1 tuples, because id 0 is reserved. The largest Feature Id depends on the array values, so preflight cannot check it: execute must check it, or size internal buffers from the Feature Attribute Matrix tuple count. Look for buffers sized by the largest Feature Id but read by the Feature Attribute Matrix tuple count, and the reverse. Also look for sparse or negative ids used directly as indices. | + +**Where each check belongs.** Structure (component shape, DataType) goes in the parameter: parameter validation runs before the filter's preflight, so a repeated check in preflight or execute is dead code, and an existing one is removed. Relationships between inputs (tuple count against a sibling DataArray, parent Attribute Matrix) go in preflight. Only value-dependent bounds go in execute. + +### SIMPLNX vocabulary for findings + +State every finding in SIMPLNX terms, so that the reader can see how the input is reachable. + +- **Attribute Matrix kinds:** *element* Attribute Matrices (*Vertex Data*, *Edge Data*, *Face Data*, *Cell Data*), whose tuple count equals the number of those elements; *Feature* Attribute Matrices (for example *Cell Feature Data*), with largest Feature Id + 1 tuples; *Ensemble* Attribute Matrices (for example *Cell Ensemble Data*). +- **Data objects:** DataArray, StringArray, NeighborList, DataPath. Say "number of tuples", "tuple shape", "number of components", "component shape" and DataType. +- **Geometries:** Image, Rectilinear Grid, Vertex, Edge, Triangle, Quad, Tetrahedral and Hexahedral Geometry. Say "number of cells" or "number of faces". +- **Parameters:** name a parameter by its display name in bold, for example **Create Bounding Box Geometries**, and say whether the failure is at parameter validation, preflight or execute. + +Write "The Feature Ids DataArray is not checked to be a child of the Image Geometry's Cell Data Attribute Matrix, so its number of tuples can differ from the number of cells". Do not write "Feature Ids may not have one value per cell". + +For each suspected defect, record `file:line`, the input that triggers it, and the expected and actual results. A suspicion is not a finding. Confirm it with a failing test or a hand calculation, or discard it. Every confirmed defect becomes a `-D` deviation entry and a regression test. A defect is a deviation even when 6.5.171 has the same defect. + +## Part 2 — Mode coverage + +Make a list of every value of every `ChoicesParameter`, every `BoolParameter` state that changes the output, and every dispatched algorithm variant. For each item, cite a test that **asserts output values** in that mode. A mode that only "runs without error" is not covered. Put each uncovered mode in the Code path coverage table as a gap with its reason. Do not omit it. + +## Part 3 — Symmetry-breaking fixtures + +A fixture can detect a swap of two axes, components, or features only if the swap changes the expected output. Design each oracle fixture so that: + +- each axis has a different value: offsets, dimensions (`X ≠ Y ≠ Z`), spacing, and a non-zero origin; +- each component of a multi-component value is different (for example, a translation of (1, 2, 3), not (1, 1, 1)); +- there are at least two features or phases, and their values are different; +- masks, feature shapes, and positions are not symmetric about the center of the geometry; and +- the fixture contains values that make truncation and rounding give different results, and values that have a sign that the filter must keep. + +If a fixture cannot break a symmetry (for example, the filter supports only cubic inputs), write the reason in the Oracle section. + +## Part 4 — Mutation check + +This part tests the tests. For each row in the Code path coverage table, make at least one small mutant of the source in that path: + +- swap an index (`[1]` → `[0]`); +- reverse a comparison (`<` → `<=`, `>` → `>=`); +- change a bound by one; +- remove a guard or branch; or +- change a constant. + +Apply one mutant at a time. Rebuild, run the filter's tests, then restore the source. A mutant is **killed** when at least one test fails. A **surviving** mutant shows a path that the tests do not check. Add an assertion that kills it, or record why the mutant is equivalent (it cannot change the output). + +Record the result as `K of N mutants killed` and list each survivor with its disposition. The `vv-tests` skill packages a helper that applies, builds, tests, and restores each mutant. + +## Part 5 — Metamorphic relations (recommended) + +A metamorphic relation connects the outputs of two runs. It does not need a hand-calculated expected value. Examples: + +- **Translation:** move the input by (a, b, c) → the output positions move by (a, b, c), and the per-feature values stay the same. +- **Axis permutation:** exchange X and Y in the input → the output exchanges X and Y. +- **Relabel:** permute the feature IDs → the per-feature outputs are permuted in the same way. +- **Scale:** multiply the spacing by s → lengths multiply by s, areas by s², and volumes by s³. + +Relations of this type find axis and index defects, and do not need a new oracle. Record them as Class 4 companions (see [`oracle_classes.md`](./oracle_classes.md)). + +## Report + +Fill the `## Defect hunt` section of the report: + +- the patterns reviewed and the reviewer; +- a findings table with columns ID, Pattern, `file:line`, Trigger, Disposition (`fixed — -D`, `not a defect — `, or `deferred — `); +- mode coverage as `M of M modes value-asserted`, with the gaps; +- the mutation result as `K of N mutants killed`, with each survivor and its disposition; and +- the metamorphic relations used, or `None`. + +"No defects found" is a valid result only when the section shows the patterns that were reviewed and the mutation result. diff --git a/docs/vv_templates/deviation_template.md b/docs/vv_templates/deviation_template.md index d1e0428c6c..6b3aa96aee 100644 --- a/docs/vv_templates/deviation_template.md +++ b/docs/vv_templates/deviation_template.md @@ -1,12 +1,16 @@ -# Deviations from DREAM3D 6.5.171: +# Deviations from DREAM.3D v6.5.171: -This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent. +This file has two parts. Part 1 lists every documented behavioral difference between this SIMPLNX filter and its DREAM.3D v6.5.171 equivalent. Part 2 lists SIMPLNX defects and SIMPLNX behavior changes that are not differences from DREAM.3D v6.5.171: defects fixed in SIMPLNX, SIMPLNX defects that are not fixed, and changes to SIMPLNX output between DREAM3D-NX releases. Omit a part that has no entries. -Entries are referenced by stable ID (`-D`) from the V&V report and from public migration guidance. The ID is stable across renames; the Filter UUID field is the permanent cross-reference anchor. +Entries are referenced by stable ID (`-D`) from the V&V report and from public migration guidance. Both parts use one ID sequence. The ID is stable across renames; the Filter UUID field is the permanent cross-reference anchor. + +A user reads this file. Each entry has only the fields shown below: the table, Symptom, Root cause, Affected users, Recommendation, and an optional Evidence paragraph. Do not add other fields. Outside the Evidence paragraph, name DREAM3D-NX release versions, not branches, commits, or PRs. --- -## -D1 +## Part 1 — Differences from DREAM.3D v6.5.171 + +### -D1 | Field | Value | |---|---| @@ -26,7 +30,7 @@ Entries are referenced by stable ID (`-D`) from the V&V report an --- -## -D2 +### -D2 | Field | Value | |---|---| @@ -44,6 +48,32 @@ Entries are referenced by stable ID (`-D`) from the V&V report an --- +## Part 2 — SIMPLNX defects and behavior changes + +### -D3 + +| Field | Value | +|---|---| +| **Deviation ID** | `-D3` | +| **Filter UUID** | `` | +| **Status** | active *or* superseded by `-D` *or* retired YYYY-MM-DD | +| **Affected released versions** | *DREAM3D-NX v through v. State whether DREAM.3D v6.5.171 was affected.* | +| **Fixed in** | *DREAM3D-NX v* *or* *The DREAM3D-NX release after v* *or* *Not fixed* | + +**Symptom:** ** + +**Root cause:** *bug | behavior change* +*One paragraph explaining the technical mechanism. Cite source files and line ranges where helpful.* + +**Affected users:** ** + +**Recommendation:** *trust SIMPLNX | * +*One sentence justifying the recommendation.* + +**Evidence:** *Optional. One short paragraph for engineers: the tests, release sources, legacy source, or comparison artifacts (`vv_work:/`) that confirm this entry. Only this paragraph can cite PR numbers, commits, or release branches.* + +--- + ## Examples (delete this section before sign-off) ### Precision example diff --git a/docs/vv_templates/oracle_classes.md b/docs/vv_templates/oracle_classes.md index 1fadb93438..020970744e 100644 --- a/docs/vv_templates/oracle_classes.md +++ b/docs/vv_templates/oracle_classes.md @@ -1,10 +1,10 @@ # Oracle Classes — Detailed Reference -The DREAM3D-NX V&V policy (see [`vv_policy.md`](./vv_policy.md)) names five oracle classes — five ways to establish what the *correct* output of a filter should be, independently of any DREAM3D implementation. The policy's table is a quick lookup. This document is the long-form explanation: what each class actually means in practice, what kinds of bugs it catches, what its weaknesses are, and how to choose between them when designing the oracle for a new (or retroactive) V&V pass. +The DREAM3D-NX V&V policy (see [`vv_policy.md`](./vv_policy.md)) names five oracle classes — five ways to establish what the *correct* output of a filter should be, independently of any DREAM.3D or DREAM3D-NX implementation. The policy's table is a quick lookup. This document is the long-form explanation: what each class actually means in practice, what kinds of bugs it catches, what its weaknesses are, and how to choose between them when designing the oracle for a new (or retroactive) V&V pass. ## What an oracle is, and why this matters -An **oracle** is the source of truth you compare the filter against. The policy's "one ordering rule" is to pick the oracle *before* running any DREAM3D 6.5.171 comparison — because if the oracle is wrong, you will happily confirm a buggy filter. The oracle must be independent of the implementation it validates: it cannot be the filter's own output, and it cannot be the legacy DREAM3D's output (which is "trusted by reputation, not by proof"). +An **oracle** is the source of truth you compare the filter against. The policy's "one ordering rule" is to pick the oracle *before* running any DREAM.3D v6.5.171 comparison — because if the oracle is wrong, you will happily confirm a buggy filter. The oracle must be independent of the implementation it validates: it cannot be the filter's own output, and it cannot be the legacy DREAM.3D's output (which is "trusted by reputation, not by proof"). The five classes differ in **what they trust** (math, a library, a paper, properties, or a person) and **how durable** that trust is over time. The policy ranks them by trust durability, and prefers oracles that are stable, reproducible, and external to the code being tested. @@ -118,6 +118,7 @@ The rename was applied 2026-06-10 across all test files, V&V reports, deviations - **Sum-to-one:** `Σ phase_fraction[p] == 1.0` for any cell. - **Symmetry under transformation:** `Filter(Rotate(input)) == Rotate(Filter(input))` for rotation-equivariant filters. - **Idempotence:** `Filter(Filter(input)) == Filter(input)` for filters that should converge in one pass. +- **Metamorphic relations:** relate the outputs of two runs. Translate the input by (a, b, c) → the output moves by (a, b, c). Exchange X and Y → the output exchanges X and Y. Permute the feature IDs → the per-feature outputs are permuted. Use different values on each axis, or the relation cannot find an axis defect. See [`defect_hunt.md`](./defect_hunt.md) Part 5. **Strengths:** @@ -216,9 +217,9 @@ until you find one. A common trap is to mistake one of the following for an oracle. None of these qualify: - **The filter's own output**, captured on a previous date and saved as a "golden" exemplar. This is circular: any bug present at capture time becomes the new "correct" answer, and the test then confirms the bug forever. -- **The legacy DREAM3D 6.5.171's output.** Treated by reputation, not by proof. Useful for diff explanation (see [`vv_policy.md`](./vv_policy.md)), not for correctness. +- **The legacy DREAM.3D v6.5.171's output.** Treated by reputation, not by proof. Useful for diff explanation (see [`vv_policy.md`](./vv_policy.md)), not for correctness. - **Another simplnx filter's output**, when both filters might share the same bug (e.g., both call into a buggy shared utility). -- **A previous version of the same filter** ("my SIMPL implementation works, so I'll just diff against that"). Identical to the legacy DREAM3D trap. +- **A previous version of the same filter** ("my SIMPL implementation works, so I'll just diff against that"). Identical to the legacy DREAM.3D trap. If the existing exemplar in the data archive was generated from one of the above, it should be regenerated from a real oracle as part of the V&V pass — this is the "circular oracle" pattern: a golden file captured from the filter's own (or legacy's) output, which then pins any bug present at capture time as the new "correct" answer. diff --git a/docs/vv_templates/provenance_template.md b/docs/vv_templates/provenance_template.md index b80c494c9a..48a587bfb7 100644 --- a/docs/vv_templates/provenance_template.md +++ b/docs/vv_templates/provenance_template.md @@ -17,6 +17,8 @@ One sidecar per archive. The archive name and SHA512 must match `download_test_d | **Generated on** | *YYYY-MM-DD* | | **Generated at commit** | ** | + + ## How it was generated *Describe in 2–4 sentences how the archive's contents were produced. Reference any pipeline (`.d3dpipeline`) or script (`.py`, `.m`) by file path. State whether inputs were from a public dataset, synthetic generation, or hand-construction.* diff --git a/docs/vv_templates/report_gates.md b/docs/vv_templates/report_gates.md index 6ffbdadbd8..ffa14bd36d 100644 --- a/docs/vv_templates/report_gates.md +++ b/docs/vv_templates/report_gates.md @@ -2,29 +2,34 @@ A section of `src/Plugins/

/vv/.md` is "done" when all its gates pass. Sections may be worked in any order. -**The only ordering rule:** the **Oracle** is chosen *before* any DREAM3D 6.5.171 comparison is run. 6.5.171 is never the source of truth — it is the *thing being compared against* the independently-established oracle. +**The only ordering rule:** the **Oracle** is chosen *before* any DREAM.3D v6.5.171 comparison is run. 6.5.171 is never the source of truth — it is the *thing being compared against* the independently-established oracle. --- ## Header table -- [ ] Plugin, SIMPLNX UUID, legacy DREAM3D equivalent (or "None") filled in +- [ ] Plugin, SIMPLNX UUID, SIMPLNX Human Name, and legacy DREAM.3D v6.5.171 equivalent (or "None") filled in +- [ ] No header rows other than the template rows; nothing after the Status value - [ ] Status reflects current state: `DRAFT` | `READY FOR REVIEW` | `COMPLETE` - [ ] Verified-commit field present (filled in at SBIR deliverable assembly) -- [ ] Sign-off line has named engineer(s) and date at sign-off +- [ ] Sign-off line uses the form `, YYYY-MM-DD; second engineer: , YYYY-MM-DD`, with full names, ISO dates, and no email addresses +- [ ] The second engineer is the person who approved the PR that contains the V&V work for the filter ## At a glance The dashboard a reviewer reads first. Lets a reviewer decide in 30 seconds whether they need to dig into the long-form sections below. -- [ ] All 8 rows present: Algorithm Relationship, Oracle (confirmed), Code paths enumerated, Tests today, Exemplar archive, Legacy comparison, Bug flags, V&V phase +- [ ] All 9 rows present: Algorithm Relationship, Oracle (confirmed), Code paths enumerated, Tests today, Defect hunt, Exemplar archive, Legacy comparison, Bug flags, V&V phase (reports that reached `READY FOR REVIEW` before 2026-10-02 may omit Defect hunt until their next V&V change) - [ ] Each cell is one sentence to one short paragraph — not a single word, not a full subsection. If a row needs more than ~3 sentences, that detail belongs in the long-form section and the dashboard summarizes it - [ ] **Algorithm Relationship** row names the legacy equivalent (or "no legacy equivalent") and the classification — must agree with the long-form `## Algorithm Relationship` section - [ ] **Oracle (confirmed)** row names the Class number(s) and the encoded test fixture(s). Use "confirmed" only when the oracle has been applied and the test passes; otherwise write "tentative" or "in progress" - [ ] **Code paths enumerated** row states `N of M exercised` — agrees with the long-form `## Code path coverage` count - [ ] **Tests today** row gives the test-case count and a one-phrase shape of coverage (parameter sweep, positive/negative/conversion, etc.) - [ ] **Exemplar archive** row names the archive and flags retired/replaced archives (cross-reference the long-form `## Exemplar archive` SHA512) -- [ ] **Legacy comparison** row is `Run` / `Not run` plus a one-sentence headline. The comparison is always framed as SIMPLNX vs DREAM3D 6.5.171. When a deviation's root cause was proven by surgically patching a local build of the legacy source, describe it exactly that way — the report must NOT name any other legacy version number (the patched build is internal proof tooling, not a shipping comparison target). "Not run" must include a brief reason ("design-by-inspection — pure port", "legacy binary unavailable", "deferred to Phase 9") +- [ ] **Legacy comparison** row is `Run` / `Not run` plus a one-sentence headline. The comparison is always framed as SIMPLNX vs DREAM.3D v6.5.171. When a deviation's root cause was proven by surgically patching a local build of the legacy source, describe it exactly that way — the report must NOT name any other legacy version number (the patched build is internal proof tooling, not a shipping comparison target). "Not run" must include a brief reason ("design-by-inspection — pure port", "legacy binary unavailable", "deferred to Phase 9") +- [ ] A `Run` result comes from a current `nxrunner` build that contains the current algorithm. A comparison run before the last algorithm change does not count; run it again +- [ ] The comparison artifacts (pipelines, inputs, outputs, and a ReadMe with the result) are uploaded to `vv_work:/` on OneDrive. If that folder does not exist, generate the artifacts and upload them +- [ ] **Defect hunt** row gives the number of confirmed defects, `M of M modes value-asserted`, and `K of N mutants killed` — agrees with the long-form `## Defect hunt` section - [ ] **Bug flags** row is `None` or a list of deviation IDs flagged as suspected bugs (not all deviations are bugs; only those classified as bug under the root-cause taxonomy) - [ ] **V&V phase** row lists which phases of the workflow are complete and what is outstanding — drives the Status field in the header table @@ -57,6 +62,7 @@ For detailed explanations of each class — with examples, strengths and weaknes - [ ] One-line description of how oracle was applied - [ ] Encoded test reference: `::` exists and is greppable - [ ] N fixtures stated; all pass at the verified commit +- [ ] Fixtures break symmetry: different values per axis and per component, `X ≠ Y ≠ Z` dimensions, a non-zero origin, and two or more features or phases with different values. A fixture that cannot break a symmetry has a stated reason (see [`defect_hunt.md`](./defect_hunt.md) Part 3) - [ ] Second-engineer review of oracle design, OR documented skip reason ## Code path coverage @@ -70,6 +76,27 @@ For detailed explanations of each class — with examples, strengths and weaknes - [ ] If `N < M`: each uncovered path appears as its own table row with `*Not directly tested. *` in the Test case cell — paths are **never silently omitted**. Acceptable reasons include: low-value loop-guard, exercised implicitly by shipping pipelines (name one), requires cancel-signal injection, deferred to integration test - [ ] Each covered path maps to ≥1 named test case (`TEST_CASE` name or `DYNAMIC_SECTION` label as it appears in the test source) - [ ] Parameter-dependent paths: every combination of interest represented (don't trust a single test case to cover the parameter cube) +- [ ] Every `ChoicesParameter` value, output-changing `BoolParameter` state, and dispatched algorithm variant has a row whose test **asserts output values** in that mode. A test that only runs the mode without error does not cover it + +## Defect hunt + +See [`defect_hunt.md`](./defect_hunt.md). + +- [ ] Bug-pattern review done on the algorithm, its helpers, and the filter preflight/execute — reviewer named +- [ ] Sibling branches (modes, axes, in-core vs. OOC variants) compared side by side +- [ ] Findings table present. Each row has ID, pattern, `file:line`, trigger, and disposition. Each confirmed defect has a `-D` deviation and a regression test +- [ ] Mode coverage stated as `M of M modes value-asserted`; gaps also appear in Code path coverage +- [ ] Mutation check done with at least one mutant per code-path row. Result stated as `K of N mutants killed`. Each survivor has an added assertion or an equivalent-mutant reason +- [ ] Metamorphic relations listed, or `None` + +## Bugs found and fixed + +- [ ] Every confirmed defect that the V&V found and fixed is listed, also when an earlier PR fixed it +- [ ] Each row uses a stable `-D` ID that has an entry in `vv/deviations/.md` +- [ ] Affected released versions come from `docs/dream3d_nx_release_dates.md`, and state whether DREAM.3D v6.5.171 was affected +- [ ] The lead sentence says which release contains each fix: a named DREAM3D-NX release, or the release after the latest affected release. No branch, commit, or PR wording +- [ ] A confirmed defect that is not fixed is in the `Bug flags` row, not in this table +- [ ] `None.` when the V&V found no defects ## Test inventory @@ -90,7 +117,7 @@ For detailed explanations of each class — with examples, strengths and weaknes - what oracle output was canonical - [ ] If the archive was regenerated during V&V to fix a circular-oracle situation → documented in the sidecar -## Deviations from DREAM3D 6.5.171 +## Deviations from DREAM.3D v6.5.171 - [ ] Comparison was run on at least one fixture (named in the report) - [ ] If no deviations: a one-line confirmation that 6.5.171 and SIMPLNX outputs matched within tolerance diff --git a/docs/vv_templates/report_template.md b/docs/vv_templates/report_template.md index b8f7867bf4..4661d2e9bd 100644 --- a/docs/vv_templates/report_template.md +++ b/docs/vv_templates/report_template.md @@ -2,14 +2,33 @@ + + | | | |--------|--------------| | Plugin | | | SIMPLNX UUID | | -| DREAM3D 6.5.171 equivalent | *or* None (new filter) | +| SIMPLNX Human Name | | +| DREAM.3D v6.5.171 equivalent | *or* None (new filter) | | Verified commit | ** | -| Status | DRAFT | -| Sign-off | ** | +| Status | DRAFT *or* READY FOR REVIEW *or* COMPLETE | +| Sign-off | *, YYYY-MM-DD; second engineer: , YYYY-MM-DD* | + + ## At a glance @@ -22,11 +41,12 @@ A scannable dashboard for reviewers. Each row is one sentence to one short parag | Code paths enumerated | *N of M exercised; one phrase about any uncovered paths.* | | Tests today | *N test cases — one phrase about coverage (e.g., "parameter sweep over (Tolerance, NumberOfNeighbors)", "1 positive + 1 negative + 1 SIMPL backward-compat").* | | Exemplar archive | *`` — one phrase on what it provides (inputs only, inputs + outputs, retired/replaced).* | -| Legacy comparison | *Run / Not run — one-sentence headline (bit-identical, N deviations). Always framed as SIMPLNX vs DREAM3D 6.5.171; root-cause proof via a patched local build of the legacy source is described without naming a legacy version or commit.* | +| Legacy comparison | *Run / Not run — one-sentence headline (bit-identical, N deviations). Always framed as SIMPLNX vs DREAM.3D v6.5.171; a run on a non-stock legacy build is called "a local proof version" and names no other legacy version or commit.* | +| Defect hunt | *N confirmed defects; M of M modes value-asserted; K of N mutants killed.* | | Bug flags| *None / list of deviation IDs flagged as suspected bugs.* | | V&V phase| *Which phases of the V&V workflow are complete; what is outstanding before status promotion.* | -For worked instances see `src/Plugins/OrientationAnalysis/vv/BadDataNeighborOrientationCheckFilter.md` and `src/Plugins/OrientationAnalysis/vv/ComputeAvgCAxesFilter.md` (on `topic/vv/compute_avg_caxis`). +For a worked instance see `src/Plugins/SimplnxCore/vv/DBSCANFilter.md`. ## Summary @@ -50,14 +70,30 @@ For worked instances see `src/Plugins/OrientationAnalysis/vv/BadDataNeighborOrie ## Bugs found and fixed - + + +*All defects in this table are fixed. The fixes will be in the DREAM3D-NX release after v.* + +*If a released version already contains some of the fixes, add:* *DREAM3D-NX v contains the fix for `-D`.* + +| Deviation | Defect | Affected released versions | Resolution | +|-----------|--------|----------------------------|------------| +| `-D1` | *State the defect and its effect.* | *DREAM.3D v6.5.171; DREAM3D-NX v through v.* | *State what the filter does now.* | +| `-D2` | *State the defect and its effect.* | *DREAM.3D v6.5.171 only. DREAM3D-NX was not affected.* | *State what the filter does now.* | + +## Defect hunt + +*Reviewer:* ** — patterns from [`defect_hunt.md`](../../../../docs/vv_templates/defect_hunt.md) Part 1, with sibling branches compared side by side. + +| ID | Pattern | Location | Trigger | Disposition | +|----|---------|----------|---------|-------------| +| H1 | *e.g. Copy-paste drift* | *`.cpp:`* | *e.g. centroid offset (1, 2, 3)* | *fixed — `-D1`* / *not a defect — * / *deferred — * | + +*Mode coverage:* *M of M modes value-asserted. List any gaps.* -*This branch fixes all defects in this table. The fixes will be in the DREAM3D-NX release after v.* +*Mutation check:* *K of N mutants killed. List each survivor and its disposition.* -| Deviation | Defect | Affected released versions | Resolution in this branch | -|-----------|--------|----------------------------|---------------------------| -| `-D1` | *State the defect and its effect.* | *DREAM.3D 6.5.171; DREAM3D-NX v through v.* | *State how the verified branch fixes the defect.* | -| `-D2` | *State the defect and its effect.* | *DREAM.3D 6.5.171 only. DREAM3D-NX was not affected.* | *State how the verified branch fixes or prevents the defect.* | +*Metamorphic relations:* *e.g. translation by (1, 2, 3) moves the output by (1, 2, 3)* / *None.* ## Code path coverage @@ -77,7 +113,7 @@ Source: *`src/Plugins/

/src/

/Filters/Algorithms/.cpp` ( lines) When a path is intentionally not covered, write `*Not directly tested. *` in the Test case cell rather than omitting the row. Uncovered paths must still appear in the table so the reader can audit the gap. -*The `Phase` column can be dropped (or renamed `Pass` / `Stage`) when the algorithm has no obvious staged structure. For a worked two-pass instance see `src/Plugins/OrientationAnalysis/vv/BadDataNeighborOrientationCheckFilter.md`; for a worked three-phase instance see `src/Plugins/OrientationAnalysis/vv/ComputeAvgCAxesFilter.md` (on `topic/vv/compute_avg_caxis`).* +*The `Phase` column can be dropped (or renamed `Pass` / `Stage`) when the algorithm has no obvious staged structure. For a worked two-pass instance see `src/Plugins/OrientationAnalysis/vv/BadDataNeighborOrientationCheckFilter.md`; for a worked three-phase instance see `src/Plugins/OrientationAnalysis/vv/ComputeAvgCAxesFilter.md`.* ## Test inventory @@ -91,7 +127,7 @@ When a path is intentionally not covered, write `*Not directly tested. `* - **Provenance:** *`src/Plugins/

/vv/provenance/.md`* -## Deviations from DREAM.3D 6.5.171 +## Deviations from DREAM.3D v6.5.171 *Either:* @@ -99,7 +135,7 @@ When a path is intentionally not covered, write `*Not directly tested. .md` for the root cause, affected users, and recommendation for each deviation. +See `vv/deviations/.md` for the root cause, affected users, and recommendation for each deviation. List only the Part 1 entries of that file here. Part 2 entries are SIMPLNX defects and SIMPLNX behavior changes; the `Bugs found and fixed` table and the `Bug flags` row refer to them. | Deviation | Observed difference | |-----------|---------------------| diff --git a/docs/vv_templates/vv_policy.md b/docs/vv_templates/vv_policy.md index 2a58017b98..6865a82866 100644 --- a/docs/vv_templates/vv_policy.md +++ b/docs/vv_templates/vv_policy.md @@ -1,6 +1,6 @@ # DREAM3D-NX Filter V&V Policy (v2) -This is the short-form policy for filter Verification & Validation (V&V) in DREAM3D-NX. It exists to support MTR SBIR deliverables and the public DREAM3D → DREAM3D-NX migration guide. +This is the short-form policy for filter Verification & Validation (V&V) in DREAM3D-NX. It exists to support MTR SBIR deliverables and the public DREAM.3D → DREAM3D-NX migration guide. Read this once. The deliverable is a one-page report per filter; the working details live in the templates and gates docs alongside this file. @@ -8,16 +8,22 @@ Read this once. The deliverable is a one-page report per filter; the working det ## Why -DREAM3D 6.5.171 is trusted by reputation, not by proof. It has been in the field long enough that users rely on its numbers, but it was never subjected to V&V rigor. We therefore cannot treat "matches 6.5.171" as a correctness check — it is a **diff-explanation** check. +DREAM.3D v6.5.171 is trusted by reputation, not by proof. It has been in the field long enough that users rely on its numbers, but it was never subjected to V&V rigor. We therefore cannot treat "matches 6.5.171" as a correctness check — it is a **diff-explanation** check. Each filter must first be shown correct **independently** of 6.5.171 against an oracle, and only then diffed against legacy to produce user-facing migration guidance. ## The one ordering rule -> **Pick the oracle before running any DREAM3D 6.5.171 comparison.** +> **Pick the oracle before running any DREAM.3D v6.5.171 comparison.** Everything else — code-path enumeration, test inventory, exemplar provenance, algorithm review, documentation — can be worked in any order. +## Find bugs, not only document behavior + +An oracle shows that the filter agrees with the fixtures the engineer chose. It does not show that those fixtures can find a defect. Every V&V therefore includes a **defect hunt**: a bug-pattern review, a check that each mode has a value assertion, symmetry-breaking fixtures, and a mutation check of the tests. Do the defect hunt after code-path enumeration and before the oracle fixtures are final. See [`defect_hunt.md`](./defect_hunt.md). + +Reports that reached `READY FOR REVIEW` before 2026-10-02 do not need a `## Defect hunt` section until their next V&V change. + ## Oracle classes Every filter must be verified against at least one of the following. Classes 1–4 are preferred; Class 5 requires a documented justification for why no Class 1–4 oracle was feasible. @@ -53,10 +59,16 @@ The verified state is pinned by **(commit hash, archive SHA512)**. The commit ca | File | What it is | |---|---| | [`report_template.md`](./report_template.md) | Empty report — copy into `src/Plugins/

/vv/.md` | +| [`defect_hunt.md`](./defect_hunt.md) | Bug-pattern review, mode coverage, symmetry-breaking fixtures, and mutation check — required for every V&V | | [`report_gates.md`](./report_gates.md) | Per-section "Done when:" checklists — reference while filling in the report | | [`deviation_template.md`](./deviation_template.md) | Empty deviation file — copy into `src/Plugins/

/vv/deviations/.md` | | [`provenance_template.md`](./provenance_template.md) | Empty exemplar-provenance sidecar — copy per exemplar archive | -| [`commit_template.md`](./commit_template.md) | Standard commit message format for landing a completed V&V cycle — use at step 6 of the engineer workflow below | +| [`commit_template.md`](./commit_template.md) | Standard commit message format for landing a completed V&V cycle — use at step 7 of the engineer workflow below | +| [`vv_recertification.md`](./vv_recertification.md) | When V&V work happens after a filter has a V&V record, the single V&V branch, and the legacy comparison packages | + +## After a filter has a V&V record + +A change to a V&V'd filter follows [`vv_recertification.md`](./vv_recertification.md). It says which changes need V&V work in the same PR, where that work is done, and how legacy comparisons are run again. ## Engineer workflow @@ -64,9 +76,10 @@ The verified state is pinned by **(commit hash, archive SHA512)**. The commit ca 2. Decide the oracle class for this filter (write it down). 3. Run `python scripts/vv_init.py ` to scaffold the report and deviation files in the plugin tree. 4. Open `report_gates.md` in a second tab. -5. Work each section in any order. A section is "done" when all its gates pass. -6. When all gates green, set `Status: READY FOR REVIEW`, push a `vv/` branch with a commit following [`commit_template.md`](./commit_template.md). -7. After sign-off, set `Status: COMPLETE`. Verified commit hash is filled in at SBIR deliverable assembly. +5. After you enumerate the code paths, do the defect hunt ([`defect_hunt.md`](./defect_hunt.md)) before the oracle fixtures are final. +6. Work each section in any order. A section is "done" when all its gates pass. +7. When all gates green, set `Status: READY FOR REVIEW`, push a `vv/` branch with a commit following [`commit_template.md`](./commit_template.md). +8. After sign-off, set `Status: COMPLETE`. Verified commit hash is filled in at SBIR deliverable assembly. ## Status tracking across filters diff --git a/docs/vv_templates/vv_recertification.md b/docs/vv_templates/vv_recertification.md new file mode 100644 index 0000000000..4a596768eb --- /dev/null +++ b/docs/vv_templates/vv_recertification.md @@ -0,0 +1,48 @@ +# V&V Recertification Policy + +This document says when V&V work happens after a filter has a V&V record, and how that work is organized. It applies to every filter that has a report in `src/Plugins//vv/`. Read [`vv_policy.md`](./vv_policy.md) first; it defines what a V&V record contains. + +Each rule has a status: + +- **Agreed** — follow it. +- **Proposed** — the team has not decided yet. Follow it as the default, and raise any problem with it. + +## 1. What triggers V&V work (Proposed) + +The trigger is what a change does to the filter's output, not the fact that the change touches the filter's code. + +| Class | Typical changes | V&V action in the same PR | +|---|---|---| +| **A. Output unchanged for valid input** | Refactoring, out-of-core storage changes, performance work, messages and progress, a new check that rejects invalid input | None. The filter's existing V&V tests must pass **with no change to their expected values, exemplars, or tolerances**. That result is the evidence. | +| **B. Output changes for valid input** | A bug fix that changes values, a precision change (for example float32 to float64 accumulation), a changed default | A scoped V&V update: add the new tests to the report's test inventory, add a Part 2 deviation entry, derive any changed expected value independently, and mark the Legacy comparison as needing a new run. A full recertification is not required. | +| **C. Release gate** | Every V&V'd filter whose source changed since the last release | Run the legacy comparison packages again on the release candidate, update the Legacy comparison rows, and confirm each Status. | + +**The decision rule.** A change is Class B when it edits an oracle assertion, an expected value, an exemplar archive, or a comparison tolerance in a V&V'd filter's tests. Otherwise it is Class A. A reviewer can check this rule from the diff. + +**What a failing V&V test means.** + +- An existing oracle test fails, or its expected value must change: the V&V record was wrong for that case. Either the oracle was wrong or the record certified defective output. Correct the record, not only the code. +- A new test fails on the old code and no existing test covered the case: the oracle is still valid, but the V&V had a coverage gap. Add the test to the test inventory and add a Part 2 deviation entry. + +## 2. One branch for V&V work (Agreed) + +- All V&V work is on one branch and one draft PR until the V&V policy and the report template are stable: the policy and template changes, the V&V records, and the code fixes for defects in V&V'd filters. +- **Why:** a template change after a merge would make the merged records out of date and require another pass over all records. One branch prevents a series of merges that later changes would make irrelevant. +- **Commits:** each code fix is its own commit, one per filter, with tests that fail on the old code. A V&V record update is a separate commit. Keep code commits self-contained, so that an urgent fix (for example a crash) can move to its own PR by cherry-pick. +- **Rebase often.** Rebase onto `develop`; never merge `develop` into the branch. Let other large PRs that change the same filters merge first, and absorb their changes here. +- Other PRs (features, out-of-core work, general bug fixes) do not edit `vv/` files. When such a PR changes a V&V'd filter, record the gap on the V&V branch; do not expand the other PR. + +## 3. Legacy comparison packages (Agreed) + +A legacy comparison is **diff-explanation evidence for users who migrate from DREAM.3D v6.5.171**. It is not a correctness oracle; the analytical unit tests establish correctness (see `vv_policy.md`). + +- **Three-way run.** Each package runs the same shared legacy-format input through stock DREAM.3D v6.5.171, a local proof version (DREAM.3D v6.5.171 with surgical patches), and DREAM3D-NX. +- **The proof.** Stock DREAM.3D v6.5.171 differs from DREAM3D-NX only where a Part 1 deviation predicts it. The local proof version, which contains a fix for each such deviation, matches DREAM3D-NX. Then the documented deviations fully explain the differences. +- **When no patch is needed.** A deviation that is a configuration difference, a DREAM3D-NX-only option, or a validation check does not change the calculation; it needs no proof patch. Write "Not applicable" and the reason, never "missing". +- **Proof patches.** One commit per filter on the proof branch (`v6_5_172` in the BlueQuartz GitLab DREAM3D repository). The commit message names the deviation IDs and says what is fixed. Patch only the documented causes; no cleanup. +- **Archive.** Each package is stored in the V&V working archive (OneDrive) as `vv_work:Filter/`. Keep only the primary package; do not add extra copies or older runs, because they confuse later readers. A filter with no legacy equivalent gets a ReadMe that says so. +- **Package contents.** A ReadMe (filter, runners and their versions, cases, results, deviation-to-case-to-patch mapping, how to run again), pipelines, inputs, the three outputs, logs, comparison results, and SHA512 checksums. + +## 4. Report wording (Agreed) + +The rules for report wording are in [`report_template.md`](./report_template.md) and [`deviation_template.md`](./deviation_template.md). In short: write for a user of `develop` or a DREAM3D-NX release; name release versions, not branches, commits, or PRs; refer to DREAM.3D v6.5.171 only; call a patched legacy build "a local proof version". diff --git a/src/Plugins/OrientationAnalysis/docs/ComputeAvgCAxesFilter.md b/src/Plugins/OrientationAnalysis/docs/ComputeAvgCAxesFilter.md index 01b5921c5c..fda3d0fe55 100644 --- a/src/Plugins/OrientationAnalysis/docs/ComputeAvgCAxesFilter.md +++ b/src/Plugins/OrientationAnalysis/docs/ComputeAvgCAxesFilter.md @@ -49,7 +49,7 @@ All cell-level arrays (feature IDs, phases, quaternions) and the feature-level o ### Out-of-Core Path -Cell-level data is read in sequential 4096-tuple chunks via `copyIntoBuffer`, with separate buffers for feature IDs, cell phases, and quaternions. Feature-level accumulation is performed in a local buffer (`avgCAxesCache`) that is bulk-read at the start and bulk-written back via `copyFromBuffer` after the normalization pass. The ensemble-level crystal structures array is cached locally at startup to avoid per-element OOC overhead. +Cell-level data is read in sequential 4096-tuple chunks via `copyIntoBuffer`, with separate buffers for feature IDs, cell phases, and quaternions. Feature-level directions are accumulated and normalized in a local float64 buffer to limit rounding error for large features. Each final component is converted once to float32 and bulk-written via `copyFromBuffer`. The ensemble-level crystal structures array is cached locally at startup to avoid per-element OOC overhead. ### Performance diff --git a/src/Plugins/OrientationAnalysis/docs/ComputeFaceIPFColoringFilter.md b/src/Plugins/OrientationAnalysis/docs/ComputeFaceIPFColoringFilter.md index c2821369ca..9c45f3c33b 100644 --- a/src/Plugins/OrientationAnalysis/docs/ComputeFaceIPFColoringFilter.md +++ b/src/Plugins/OrientationAnalysis/docs/ComputeFaceIPFColoringFilter.md @@ -10,7 +10,7 @@ This **Filter** generates a pair of colors for each **Triangle** in a **Triangle Each side of a **Face** is colored using **that side's own** feature: the first color uses the first feature's orientation, its phase's crystal symmetry (Laue group), and the face normal; the second color uses the second feature's orientation, **its own** phase's crystal symmetry, and the negated (inward) face normal. This matters for multi-phase microstructures where the two features adjacent to a **Face** belong to phases with different Laue groups (for example a cubic phase against a hexagonal phase) — each side is colored with the correct symmetry for its phase. A side whose feature is invalid (a boundary/exterior face, feature id ≤ 0) is colored black. -> **Note on migration from DREAM3D 6.5.171:** earlier DREAM3D 6.5.171 colored the *second* side of every **Face** using the *first* feature's crystal symmetry, which produced incorrect colors on mixed-Laue-group faces and left the second color black on faces whose first side was the exterior. DREAM3D-NX (and DREAM3D 6.5.172) compute each side with its own phase's symmetry. See V&V deviation `ComputeFaceIPFColoringFilter-D1`. +> **Note on migration from DREAM3D 6.5.171:** DREAM3D 6.5.171 colored the *second* side of every **Face** using the *first* feature's crystal symmetry, which produced incorrect colors on mixed-Laue-group faces and left the second color black on faces whose first side was the exterior. DREAM3D-NX computes each side with its own phase's symmetry. See V&V deviation `ComputeFaceIPFColoringFilter-D1`. ------------ diff --git a/src/Plugins/OrientationAnalysis/docs/ComputeFeatureReferenceCAxisMisorientationsFilter.md b/src/Plugins/OrientationAnalysis/docs/ComputeFeatureReferenceCAxisMisorientationsFilter.md index ee5ecda4f6..2a3e05797a 100644 --- a/src/Plugins/OrientationAnalysis/docs/ComputeFeatureReferenceCAxisMisorientationsFilter.md +++ b/src/Plugins/OrientationAnalysis/docs/ComputeFeatureReferenceCAxisMisorientationsFilter.md @@ -25,7 +25,7 @@ This filter requires at least one hexagonal crystal structure phase (6/m or 6/mm ### Note -Results may differ from the DREAM3D 6.6 version by approximately 0.0001 degrees due to improved double-precision calculations for cross-platform accuracy. +Feature means are accumulated in double precision and stored as float32. This reduces accumulated rounding error for large features; results can differ from the DREAM3D 6.6 version. ### Required Input Sources @@ -49,7 +49,7 @@ Cell-level arrays are iterated directly. Crystal structure validation ensures at Cell-level data (feature IDs, phases, quaternions) is processed one Z-slice at a time. For each slice, all input arrays are bulk-read via `copyIntoBuffer` into pre-allocated slice buffers, the misorientation is computed for every cell in the slice, and results are bulk-written via `copyFromBuffer`. The feature-level average c-axes array is cached locally at startup. Crystal structures are also cached from the ensemble-level array. -A second Z-slice pass re-reads the cell-level feature IDs and the just-written misorientation output to compute the population standard deviation per feature. +A second Z-slice pass re-reads the cell-level feature IDs, phases, and the just-written misorientation output to compute the population standard deviation per feature. Both passes include only cells with a positive feature ID and a positive hexagonal phase. ### Performance diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeAvgCAxes.cpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeAvgCAxes.cpp index 6f989b57b7..9bfe98761f 100644 --- a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeAvgCAxes.cpp +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeAvgCAxes.cpp @@ -3,6 +3,7 @@ #include "OrientationAnalysis/utilities/OrientationUtilities.hpp" #include "simplnx/DataStructure/DataArray.hpp" +#include "simplnx/Utilities/DataArrayUtilities.hpp" #include "simplnx/Utilities/ImageRotationUtilities.hpp" #include "simplnx/Utilities/Math/GeometryMath.hpp" @@ -66,7 +67,8 @@ Result<> ComputeAvgCAxes::operator()() auto mergeWarnings = [&result](Result<> ioResult) { return MergeResults(std::move(result), std::move(ioResult)); }; // Cell data uses chunked bulk I/O to avoid per-element OOC access. - const auto& featureIdsStoreRef = m_DataStructure.getDataRefAs(m_InputValues->FeatureIdsArrayPath).getDataStoreRef(); + const auto& featureIds = m_DataStructure.getDataRefAs(m_InputValues->FeatureIdsArrayPath); + const auto& featureIdsStoreRef = featureIds.getDataStoreRef(); const auto& quatsStoreRef = m_DataStructure.getDataRefAs(m_InputValues->QuatsArrayPath).getDataStoreRef(); const auto& cellPhasesStoreRef = m_DataStructure.getDataRefAs(m_InputValues->CellPhasesArrayPath).getDataStoreRef(); auto& avgCAxesStoreRef = m_DataStructure.getDataRefAs(m_InputValues->AvgCAxesArrayPath).getDataStoreRef(); @@ -74,11 +76,23 @@ Result<> ComputeAvgCAxes::operator()() const usize totalPoints = featureIdsStoreRef.getNumberOfTuples(); const usize totalFeatures = avgCAxesStoreRef.getNumberOfTuples(); + auto validateNumFeatResult = ValidateFeatureIdsToFeatureAttributeMatrixIndexing(m_DataStructure, m_InputValues->AvgCAxesArrayPath, featureIds, false, m_MessageHandler, &m_ShouldCancel); + if(validateNumFeatResult.invalid()) + { + return mergeWarnings( + MakeErrorResult(-76405, fmt::format("Cell Feature Ids array '{}' must contain indices in [0, {}) for Feature Attribute Matrix '{}'. {}", m_InputValues->FeatureIdsArrayPath.toString(), + totalFeatures, m_InputValues->CellFeatureDataPath.toString(), validateNumFeatResult.errors()[0].message))); + } + if(m_ShouldCancel) + { + return result; + } + // Feature IDs select output tuples in random order, so accumulation stays // local until the final write. const usize avgCAxesElements = totalFeatures * 3; - auto avgCAxesCache = std::make_unique(avgCAxesElements); - std::fill_n(avgCAxesCache.get(), avgCAxesElements, 0.0f); + auto avgCAxesCache = std::make_unique(avgCAxesElements); + std::fill_n(avgCAxesCache.get(), avgCAxesElements, 0.0); const Eigen::Vector3d cAxis{0.0f, 0.0f, 1.0f}; Eigen::Vector3d c1{0.0f, 0.0f, 0.0f}; @@ -147,9 +161,9 @@ Result<> ComputeAvgCAxes::operator()() c1.normalize(); Eigen::Vector3d curCAxis{0.0f, 0.0f, 0.0f}; - curCAxis[0] = avgCAxesCache[cAxesIndex] / static_cast(counter[currentFeatureIdx]); - curCAxis[1] = avgCAxesCache[cAxesIndex + 1] / static_cast(counter[currentFeatureIdx]); - curCAxis[2] = avgCAxesCache[cAxesIndex + 2] / static_cast(counter[currentFeatureIdx]); + curCAxis[0] = avgCAxesCache[cAxesIndex] / static_cast(counter[currentFeatureIdx]); + curCAxis[1] = avgCAxesCache[cAxesIndex + 1] / static_cast(counter[currentFeatureIdx]); + curCAxis[2] = avgCAxesCache[cAxesIndex + 2] / static_cast(counter[currentFeatureIdx]); curCAxis.normalize(); // Antiparallel c axes represent the same hexagonal direction. @@ -159,9 +173,9 @@ Result<> ComputeAvgCAxes::operator()() c1 *= -1.0f; } - avgCAxesCache[cAxesIndex] += static_cast(c1[0]); - avgCAxesCache[cAxesIndex + 1] += static_cast(c1[1]); - avgCAxesCache[cAxesIndex + 2] += static_cast(c1[2]); + avgCAxesCache[cAxesIndex] += c1[0]; + avgCAxesCache[cAxesIndex + 1] += c1[1]; + avgCAxesCache[cAxesIndex + 2] += c1[2]; } } @@ -170,6 +184,7 @@ Result<> ComputeAvgCAxes::operator()() m_MessageHandler.sendMessage({IFilter::Message::Type::Info, "Computing final feature average C-Axis values"}); + auto avgCAxesOutput = std::make_unique(avgCAxesElements); for(usize featureIdx = 0; featureIdx < totalFeatures; featureIdx++) { if(m_ShouldCancel) @@ -181,9 +196,9 @@ Result<> ComputeAvgCAxes::operator()() if(counter[featureIdx] == 0) { // Features without a hexagonal contribution have no c-axis average. - avgCAxesCache[tupleIndex] = NAN; - avgCAxesCache[tupleIndex + 1] = NAN; - avgCAxesCache[tupleIndex + 2] = NAN; + avgCAxesOutput[tupleIndex] = NAN; + avgCAxesOutput[tupleIndex + 1] = NAN; + avgCAxesOutput[tupleIndex + 2] = NAN; } else { @@ -191,13 +206,13 @@ Result<> ComputeAvgCAxes::operator()() Eigen::Vector3d finalAvg{avgCAxesCache[tupleIndex] / static_cast(counter[featureIdx]), avgCAxesCache[tupleIndex + 1] / static_cast(counter[featureIdx]), avgCAxesCache[tupleIndex + 2] / static_cast(counter[featureIdx])}; finalAvg.normalize(); - avgCAxesCache[tupleIndex] = static_cast(finalAvg[0]); - avgCAxesCache[tupleIndex + 1] = static_cast(finalAvg[1]); - avgCAxesCache[tupleIndex + 2] = static_cast(finalAvg[2]); + avgCAxesOutput[tupleIndex] = static_cast(finalAvg[0]); + avgCAxesOutput[tupleIndex + 1] = static_cast(finalAvg[1]); + avgCAxesOutput[tupleIndex + 2] = static_cast(finalAvg[2]); } } - if(Result<> ioResult = avgCAxesStoreRef.copyFromBuffer(0, nonstd::span(avgCAxesCache.get(), avgCAxesElements)); ioResult.invalid()) + if(Result<> ioResult = avgCAxesStoreRef.copyFromBuffer(0, nonstd::span(avgCAxesOutput.get(), avgCAxesElements)); ioResult.invalid()) { return mergeWarnings(std::move(ioResult)); } diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeFeatureReferenceCAxisMisorientations.cpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeFeatureReferenceCAxisMisorientations.cpp index ee1a004533..d6a5ee98cd 100644 --- a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeFeatureReferenceCAxisMisorientations.cpp +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeFeatureReferenceCAxisMisorientations.cpp @@ -5,6 +5,7 @@ #include "simplnx/Common/Constants.hpp" #include "simplnx/DataStructure/DataArray.hpp" #include "simplnx/DataStructure/Geometry/ImageGeom.hpp" +#include "simplnx/Utilities/DataArrayUtilities.hpp" #include "simplnx/Utilities/ImageRotationUtilities.hpp" #include "simplnx/Utilities/ThrottledMessageHandler.hpp" @@ -70,13 +71,26 @@ Result<> ComputeFeatureReferenceCAxisMisorientations::operator()() "Finding the feature reference c-axis misorientation requires Hexagonal-Low 6/m or Hexagonal-High 6/mmm type crystal structures. Calculations for non Hexagonal phases will be skipped."}); } - const auto& featureIdsStoreRef = m_DataStructure.getDataRefAs(m_InputValues->FeatureIdsArrayPath).getDataStoreRef(); + const auto& featureIds = m_DataStructure.getDataRefAs(m_InputValues->FeatureIdsArrayPath); + const auto& featureIdsStoreRef = featureIds.getDataStoreRef(); const auto& quatsStoreRef = m_DataStructure.getDataRefAs(m_InputValues->QuatsArrayPath).getDataStoreRef(); const auto& cellPhasesStoreRef = m_DataStructure.getDataRefAs(m_InputValues->CellPhasesArrayPath).getDataStoreRef(); // Feature references stay local because cells access them by feature ID. const auto& avgCAxes = m_DataStructure.getDataRefAs(m_InputValues->AvgCAxesArrayPath); const usize totalFeatures = avgCAxes.getNumberOfTuples(); + auto validateNumFeatResult = ValidateFeatureIdsToFeatureAttributeMatrixIndexing(m_DataStructure, m_InputValues->AvgCAxesArrayPath, featureIds, false, m_MessageHandler, &m_ShouldCancel); + if(validateNumFeatResult.invalid()) + { + return MergeResults(std::move(result), MakeErrorResult(-9806, fmt::format("Cell Feature Ids array '{}' must contain indices in [0, {}) for Average C-Axes array '{}'. {}", + m_InputValues->FeatureIdsArrayPath.toString(), totalFeatures, m_InputValues->AvgCAxesArrayPath.toString(), + validateNumFeatResult.errors()[0].message))); + } + if(m_ShouldCancel) + { + return result; + } + const usize avgCAxesSize = totalFeatures * 3; std::vector avgCAxesLocal(avgCAxesSize); readResult = avgCAxes.getDataStoreRef().copyIntoBuffer(0, nonstd::span(avgCAxesLocal.data(), avgCAxesSize)); @@ -95,7 +109,7 @@ Result<> ComputeFeatureReferenceCAxisMisorientations::operator()() const usize numQuatComps = m_DataStructure.getDataRefAs(m_InputValues->QuatsArrayPath).getNumberOfComponents(); std::vector counts(totalFeatures, 0); - std::vector avgMisorientations(totalFeatures, 0.0f); + std::vector avgMisorientations(totalFeatures, 0.0); SizeVec3 uDims = m_DataStructure.getDataRefAs(m_InputValues->ImageGeometryPath).getDimensions(); @@ -182,7 +196,7 @@ Result<> ComputeFeatureReferenceCAxisMisorientations::operator()() outputSlice[localIdx] = static_cast(w); counts[currentFeatureIdx]++; - avgMisorientations[currentFeatureIdx] += static_cast(w); + avgMisorientations[currentFeatureIdx] += w; } } } @@ -212,7 +226,8 @@ Result<> ComputeFeatureReferenceCAxisMisorientations::operator()() } else { - featureAverages[featureId] = avgMisorientations[featureId] / static_cast(counts[featureId]); + avgMisorientations[featureId] /= static_cast(counts[featureId]); + featureAverages[featureId] = static_cast(avgMisorientations[featureId]); } } Result<> writeResult = featAvgCAxisMis.getDataStoreRef().copyFromBuffer(0, nonstd::span(featureAverages.data(), totalFeatures)); @@ -240,11 +255,28 @@ Result<> ComputeFeatureReferenceCAxisMisorientations::operator()() { return readResult; } + readResult = cellPhasesStoreRef.copyIntoBuffer(sliceOffset, nonstd::span(cellPhasesSlice.data(), sliceSize)); + if(readResult.invalid()) + { + return readResult; + } for(usize localIdx = 0; localIdx < sliceSize; localIdx++) { const int32 featureId = featureIdsSlice[localIdx]; - double diff = outputSlice[localIdx] - featureAverages[featureId]; + const int32 phaseId = cellPhasesSlice[localIdx]; + if(featureId <= 0 || phaseId <= 0) + { + continue; + } + // The first pass validated positive phase indices on participating features. + const uint32 crystalStructureType = crystalStructuresCache[phaseId]; + const bool isHex = crystalStructureType == ebsdlib::CrystalStructure::Hexagonal_High || crystalStructureType == ebsdlib::CrystalStructure::Hexagonal_Low; + if(!isHex) + { + continue; + } + const float64 diff = static_cast(outputSlice[localIdx]) - avgMisorientations[featureId]; stdevs[featureId] += (diff * diff); } } diff --git a/src/Plugins/OrientationAnalysis/test/ComputeAvgCAxesTest.cpp b/src/Plugins/OrientationAnalysis/test/ComputeAvgCAxesTest.cpp index aed6e16e3f..da82c37809 100644 --- a/src/Plugins/OrientationAnalysis/test/ComputeAvgCAxesTest.cpp +++ b/src/Plugins/OrientationAnalysis/test/ComputeAvgCAxesTest.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -37,6 +38,90 @@ const DataPath k_CrystalStructuresPath = k_DataContainerPath.createChildPath("Ce const std::string k_ComputedAvgCAxesName = "ComputedAvgCAxes"; } // namespace compute_avg_caxis_class1 +namespace +{ +Arguments CreateUniformCAxisInput(DataStructure& dataStructure, usize numCells, int32 featureId, int32 phase) +{ + using namespace compute_avg_caxis_class1; + auto* imageGeom = ImageGeom::Create(dataStructure, "DataContainer"); + imageGeom->setDimensions({numCells, 1, 1}); + const ShapeType cellShape = {1, 1, numCells}; + auto* cells = AttributeMatrix::Create(dataStructure, "CellData", cellShape, imageGeom->getId()); + imageGeom->setCellData(*cells); + AttributeMatrix::Create(dataStructure, "CellFeatureData", {2}, imageGeom->getId()); + auto* ensembles = AttributeMatrix::Create(dataStructure, "CellEnsembleData", {3}, imageGeom->getId()); + auto* quats = UnitTest::CreateTestDataArray(dataStructure, "Quats", cellShape, {4}, cells->getId()); + const float32 cosHalfAngle = static_cast(std::sqrt(3.0) / 2.0); + for(usize i = 0; i < numCells; ++i) + { + // +60 degrees about X: the sample c-axis is (0, sqrt(3)/2, 1/2). + (*quats)[4 * i] = 0.5F; + (*quats)[4 * i + 3] = cosHalfAngle; + } + UnitTest::CreateTestDataArray(dataStructure, "FeatureIds", cellShape, {1}, cells->getId())->fill(featureId); + UnitTest::CreateTestDataArray(dataStructure, "Phases", cellShape, {1}, cells->getId())->fill(phase); + auto* crystalStructures = UnitTest::CreateTestDataArray(dataStructure, "CrystalStructures", {3}, {1}, ensembles->getId()); + (*crystalStructures)[0] = ebsdlib::CrystalStructure::UnknownCrystalStructure; + (*crystalStructures)[1] = ebsdlib::CrystalStructure::Hexagonal_High; + (*crystalStructures)[2] = ebsdlib::CrystalStructure::Cubic_High; + + Arguments args = ComputeAvgCAxesFilter().getDefaultArguments(); + args.insertOrAssign(ComputeAvgCAxesFilter::k_QuatsArrayPath_Key, std::make_any(k_QuatsPath)); + args.insertOrAssign(ComputeAvgCAxesFilter::k_FeatureIdsArrayPath_Key, std::make_any(k_FeatureIdsPath)); + args.insertOrAssign(ComputeAvgCAxesFilter::k_CellPhasesArrayPath_Key, std::make_any(k_PhasesPath)); + args.insertOrAssign(ComputeAvgCAxesFilter::k_CrystalStructuresArrayPath_Key, std::make_any(k_CrystalStructuresPath)); + args.insertOrAssign(ComputeAvgCAxesFilter::k_CellFeatureAttributeMatrixPath_Key, std::make_any(compute_avg_caxis_class1::k_CellFeatureDataPath)); + args.insertOrAssign(ComputeAvgCAxesFilter::k_AvgCAxesArrayName_Key, std::make_any(k_ComputedAvgCAxesName)); + return args; +} +} // namespace + +TEST_CASE("OrientationAnalysis::ComputeAvgCAxesFilter: Feature Id Bounds", "[OrientationAnalysis][ComputeAvgCAxesFilter]") +{ + UnitTest::LoadPlugins(); + const int32 featureId = GENERATE(-1, 2, std::numeric_limits::max()); + // Probe skipped non-hexagonal cells first. REQUIRE stops the old implementation + // safely before testing the same invalid ID on a cell that would index the cache. + for(const int32 phase : {2, 1}) + { + CAPTURE(featureId, phase); + DataStructure dataStructure; + const auto args = CreateUniformCAxisInput(dataStructure, 1, featureId, phase); + ComputeAvgCAxesFilter filter; + const auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + const auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(executeResult.result); + REQUIRE(executeResult.result.errors().size() == 1); + REQUIRE(executeResult.result.errors()[0].code == -76405); + UnitTest::CheckArraysInheritTupleDims(dataStructure); + } +} + +TEST_CASE("OrientationAnalysis::ComputeAvgCAxesFilter: Large Feature Analytical Precision", "[OrientationAnalysis][ComputeAvgCAxesFilter]") +{ + UnitTest::LoadPlugins(); + DataStructure dataStructure; + const auto args = CreateUniformCAxisInput(dataStructure, 1000003, 1, 1); + ComputeAvgCAxesFilter filter; + const auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + const auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + + using namespace compute_avg_caxis_class1; + const auto outputPath = compute_avg_caxis_class1::k_CellFeatureDataPath.createChildPath(k_ComputedAvgCAxesName); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(outputPath)); + const auto& avgCAxes = dataStructure.getDataRefAs(outputPath); + // Identical orientations have the same average regardless of cell count. + // Allow float32 input/output rounding, but not error that grows during summation. + constexpr float64 k_Tolerance = 2.0e-7; + CHECK(std::abs(static_cast(avgCAxes[3])) < k_Tolerance); + CHECK(std::abs(static_cast(avgCAxes[4]) - std::sqrt(3.0) / 2.0) < k_Tolerance); + CHECK(std::abs(static_cast(avgCAxes[5]) - 0.5) < k_Tolerance); + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} + TEST_CASE("OrientationAnalysis::ComputeAvgCAxesFilter: Class 1 Oracle (hand-built dataset)", "[OrientationAnalysis][ComputeAvgCAxesFilter]") { UnitTest::LoadPlugins(); @@ -72,7 +157,7 @@ TEST_CASE("OrientationAnalysis::ComputeAvgCAxesFilter: Class 1 Oracle (hand-buil // Class 1 (Analytical) — exact-value checks for F0..F6. // Closed-form expected values derived in // src/Plugins/OrientationAnalysis/vv/ComputeAvgCAxesFilter.md and verified - // bit-identical against the DREAM3D 6.5.172 custom backport + // bit-identical against a locally patched legacy build containing the custom backport // (vv/comparisons/ComputeAvgCAxesFilter/results/three_way_comparison.txt). // --------------------------------------------------------------------------- constexpr float32 k_Tol = 1.0e-5f; @@ -125,8 +210,8 @@ TEST_CASE("OrientationAnalysis::ComputeAvgCAxesFilter: Class 1 Oracle (hand-buil // Class 4 (Invariant) — F7 precision-sensitive boundary case. // F7 was deliberately constructed so the antipodal-flip cancellation dot // product evaluates to zero in math but is precision-dependent in float32. - // The SIMPLNX faithful-float32 Eigen path produces (0, sqrt(3)/2, 0.5); - // a pure-double replay produces (0, 0, 1). These are genuinely different + // Float32 accumulation produces (0, sqrt(3)/2, 0.5), whereas float64 + // accumulation can produce (0, 0, 1). These are different // c-axis directions, not hex c~-c equivalents. After the SIMPLNX // finalize-normalize step, both have magnitude 1.0 -- so the direction is // implementation-dependent but the unit-vector invariant holds. diff --git a/src/Plugins/OrientationAnalysis/test/ComputeFeatureReferenceCAxisMisorientationsTest.cpp b/src/Plugins/OrientationAnalysis/test/ComputeFeatureReferenceCAxisMisorientationsTest.cpp index 907c528db2..1517a3c84a 100644 --- a/src/Plugins/OrientationAnalysis/test/ComputeFeatureReferenceCAxisMisorientationsTest.cpp +++ b/src/Plugins/OrientationAnalysis/test/ComputeFeatureReferenceCAxisMisorientationsTest.cpp @@ -16,6 +16,7 @@ #include #include +#include #include #include #include @@ -794,3 +795,133 @@ TEST_CASE("OrientationAnalysis::ComputeFeatureReferenceCAxisMisorientationsFilte } } } + +TEST_CASE("OrientationAnalysis::ComputeFeatureReferenceCAxisMisorientationsFilter: Feature Id bounds before statistics", "[OrientationAnalysis][ComputeFeatureReferenceCAxisMisorientationsFilter]") +{ + UnitTest::LoadPlugins(); + using namespace AnalyticalFixtures; + auto fixture = CreateScaffold(2, 1, 1, 2, 2); + (*fixture.crystalStructures)[1] = ebsdlib::CrystalStructure::Hexagonal_High; + const int32 invalidId = GENERATE(2, -1); + (*fixture.featureIds)[1] = invalidId; + (*fixture.cellPhases)[1] = 0; + + // The old first pass skips phase zero, but the second pass indexes this invalid ID. + // Cancel at the intervening progress message so the RED test reports the missing + // validation error without executing the unsafe access. Correct validation returns + // before this guard is reached. + std::atomic_bool shouldCancel = false; + const IFilter::MessageHandler guard{[&](const IFilter::Message& message) { + if(message.message.find("Computing per-feature average") != std::string::npos) + { + shouldCancel = true; + } + }}; + ComputeFeatureReferenceCAxisMisorientationsFilter filter; + const auto args = BuildArgs(); + auto preflightResult = filter.preflight(fixture.ds, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + auto executeResult = filter.execute(fixture.ds, args, nullptr, guard, shouldCancel); + SIMPLNX_RESULT_REQUIRE_INVALID(executeResult.result); + REQUIRE(executeResult.result.errors()[0].code == -9806); + REQUIRE_FALSE(shouldCancel.load()); + REQUIRE(executeResult.result.errors()[0].message.find("FeatureIds") != std::string::npos); + REQUIRE(executeResult.result.errors()[0].message.find("AvgCAxes") != std::string::npos); + UnitTest::CheckArraysInheritTupleDims(fixture.ds); +} + +TEST_CASE("OrientationAnalysis::ComputeFeatureReferenceCAxisMisorientationsFilter: Statistics exclude nonparticipating cells", + "[OrientationAnalysis][ComputeFeatureReferenceCAxisMisorientationsFilter]") +{ + UnitTest::LoadPlugins(); + using namespace AnalyticalFixtures; + auto fixture = CreateScaffold(2, 1, 3, 3, 4); + (*fixture.crystalStructures)[1] = ebsdlib::CrystalStructure::Hexagonal_High; + (*fixture.crystalStructures)[2] = ebsdlib::CrystalStructure::Cubic_High; + (*fixture.crystalStructures)[3] = ebsdlib::CrystalStructure::Hexagonal_Low; + SetCellQuat(fixture, 0, QuatFromPhiDeg(10.0f)); + SetCellQuat(fixture, 1, QuatFromPhiDeg(30.0f)); + (*fixture.cellPhases)[1] = 3; + (*fixture.cellPhases)[2] = 0; + (*fixture.cellPhases)[3] = -1; + (*fixture.cellPhases)[4] = 2; + (*fixture.featureIds)[5] = 0; + + ComputeFeatureReferenceCAxisMisorientationsFilter filter; + auto executeResult = filter.execute(fixture.ds, BuildArgs()); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + const auto& cellMisos = GetOutputCellMisos(fixture.ds); + for(usize cellIdx = 2; cellIdx < 6; ++cellIdx) + { + REQUIRE(cellMisos[cellIdx] == 0.0f); + } + // Only 10 and 30 degrees contribute: mean = 20, population stdev = 10. + REQUIRE(GetOutputFeatureAvg(fixture.ds)[1] == Approx(20.0f).margin(1.0e-4f)); + REQUIRE(GetOutputFeatureStdev(fixture.ds)[1] == Approx(10.0f).margin(1.0e-4f)); + REQUIRE(std::isnan(GetOutputFeatureAvg(fixture.ds)[2])); + REQUIRE(std::isnan(GetOutputFeatureStdev(fixture.ds)[2])); + UnitTest::CheckArraysInheritTupleDims(fixture.ds); +} + +TEST_CASE("OrientationAnalysis::ComputeFeatureReferenceCAxisMisorientationsFilter: Large constant-angle feature precision", "[OrientationAnalysis][ComputeFeatureReferenceCAxisMisorientationsFilter]") +{ + UnitTest::LoadPlugins(); + using namespace AnalyticalFixtures; + auto fixture = CreateScaffold(1000, 1000, 2, 2, 2); + (*fixture.crystalStructures)[1] = ebsdlib::CrystalStructure::Hexagonal_High; + constexpr float32 k_Angle = 13.1f; + const auto quat = QuatFromPhiDeg(k_Angle); + for(usize cellIdx = 0; cellIdx < fixture.totalCells; ++cellIdx) + { + SetCellQuat(fixture, cellIdx, quat); + } + + ComputeFeatureReferenceCAxisMisorientationsFilter filter; + auto executeResult = filter.execute(fixture.ds, BuildArgs()); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + // A constant angle has that same mean and zero variance, independently of cell count. + REQUIRE(GetOutputCellMisos(fixture.ds)[0] == Approx(k_Angle).margin(1.0e-5f)); + REQUIRE(GetOutputFeatureAvg(fixture.ds)[1] == Approx(k_Angle).margin(1.0e-5f)); + REQUIRE(GetOutputFeatureStdev(fixture.ds)[1] == Approx(0.0f).margin(1.0e-5f)); + UnitTest::CheckArraysInheritTupleDims(fixture.ds); +} + +TEST_CASE("OrientationAnalysis::ComputeFeatureReferenceCAxisMisorientationsFilter: Empty arrays", "[OrientationAnalysis][ComputeFeatureReferenceCAxisMisorientationsFilter]") +{ + UnitTest::LoadPlugins(); + using namespace AnalyticalFixtures; + auto fixture = CreateScaffold(2, 1, 1, 2, 2); + (*fixture.crystalStructures)[1] = ebsdlib::CrystalStructure::Hexagonal_High; + ComputeFeatureReferenceCAxisMisorientationsFilter filter; + + SECTION("Empty cells have no contributing orientations") + { + auto resizeResult = fixture.cellAM->resizeTuples({0, 1, 2}); + SIMPLNX_RESULT_REQUIRE_VALID(resizeResult); + fixture.geom->setDimensions({2, 1, 0}); + auto executeResult = filter.execute(fixture.ds, BuildArgs()); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + REQUIRE(GetOutputCellMisos(fixture.ds).getNumberOfTuples() == 0); + REQUIRE(std::isnan(GetOutputFeatureAvg(fixture.ds)[1])); + REQUIRE(std::isnan(GetOutputFeatureStdev(fixture.ds)[1])); + } + + SECTION("Empty Average C-Axes cannot be indexed by a cell") + { + auto resizeResult = fixture.featureAM->resizeTuples({0}); + SIMPLNX_RESULT_REQUIRE_VALID(resizeResult); + auto executeResult = filter.execute(fixture.ds, BuildArgs()); + SIMPLNX_RESULT_REQUIRE_INVALID(executeResult.result); + REQUIRE(executeResult.result.errors()[0].code == -9806); + } + + SECTION("Empty Crystal Structures has no hexagonal phase") + { + auto resizeResult = fixture.ensembleAM->resizeTuples({0}); + SIMPLNX_RESULT_REQUIRE_VALID(resizeResult); + auto executeResult = filter.execute(fixture.ds, BuildArgs()); + SIMPLNX_RESULT_REQUIRE_INVALID(executeResult.result); + REQUIRE(executeResult.result.errors()[0].code == -9802); + } + UnitTest::CheckArraysInheritTupleDims(fixture.ds); +} diff --git a/src/Plugins/OrientationAnalysis/vv/BadDataNeighborOrientationCheckFilter.md b/src/Plugins/OrientationAnalysis/vv/BadDataNeighborOrientationCheckFilter.md index f2f030cce0..cd9aa50810 100644 --- a/src/Plugins/OrientationAnalysis/vv/BadDataNeighborOrientationCheckFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/BadDataNeighborOrientationCheckFilter.md @@ -1,176 +1,153 @@ # V&V Report: BadDataNeighborOrientationCheckFilter -| | | -|-----------|--------------------------| -| Plugin | OrientationAnalysis | -| SIMPLNX UUID | `3f342977-aea1-49e1-a9c2-f73760eba0d3` | -| SIMPLNX Human Name | Neighbor Orientation Comparison (Bad Data)| -| DREAM3D 6.5.171 equivalent | `BadDataNeighborOrientationCheck` — `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/BadDataNeighborOrientationCheck.{h,cpp}` | -| Verified commit | ** | -| Status | COMPLETE | -| Sign-off | *Nathan Young (algorithm rewrite + initial dataset, PR #1499, 2026-02-02) — Michael Jackson (V&V cycle completion, 2026-06-01)* | +| | | +|--------|--------------| +| Plugin | OrientationAnalysis | +| SIMPLNX UUID | `3f342977-aea1-49e1-a9c2-f73760eba0d3` | +| SIMPLNX Human Name | Neighbor Orientation Comparison (Bad Data) | +| DREAM.3D v6.5.171 equivalent | `BadDataNeighborOrientationCheck` | +| Verified commit | ** | +| Status | COMPLETE | +| Sign-off | Nathan Young, 2026-02-02; second engineer: Michael Jackson, 2026-06-01 | ## At a glance -| Aspect | Current state | -|------------------------|--------------------------| -| Algorithm Relationship | **Port** of legacy `BadDataNeighborOrientationCheck::execute()`. Same two-pass iterative-decay structure; SIMPLNX bundles two legacy bug fixes (D1, D2 — PR #1499) and a SIMPLNX-side float-π precision fix. | -| Oracle (confirmed) | **Class 1 (Analytical) primary** — engineer's hand-derived `expectedMask` arrays for all 27 algorithmic fixtures, mirrored from `bad_data_neighbor_orientation_check_v2/test_design.md`. **Class 4 (Invariant) companion** — monotonicity + no-degrade asserted via `ClassFourInvariants` helper across all base fixtures and a dedicated idempotence test. | -| Code paths enumerated | 9 of 11 exercised; cancellation and the unused-Laue sentinel guard are not directly tested. | -| Tests today | 33 registered cases plus 2 hidden HDF5 cases (Bool and UInt8 masks). Both serial CTest selections pass 33/33. | -| Exemplar archive | `bad_data_neighbor_orientation_check_v2.tar.gz` — **INPUT** `.dream3d` files only (one per case). Expected outputs are inline `expectedMask` literals in the test source. Class 1 oracle source-of-truth (`test_design.md`) bundled in the local archive copy. | -| Legacy comparison | **Run** against DREAM3D 6.5.171 on all 27 algorithmic fixtures. 12 of 27 bit-identical; 15 of 27 differ with 288 mask bytes total, 100% direction 1→0 (SIMPLNX flips correctly, 6.5.171 misses). All observed diffs trace to D1.| -| Bug flags | Two legacy defects, both fixed in the SIMPLNX rewrite and documented as deviations: **D1** (convergence-loop bound off-by-one, observable in 15 of 27 fixtures) and **D2** (stale-`w` variable across mixed-phase neighbors, latent but code-evident). | -| V&V phase | The original COMPLETE status and sign-off are retained. Section 4.3 recertification adds real-HDF5 reverse-slice cascade coverage and confirms both builds. | +| Aspect | Current state | +|--------|---------------| +| Algorithm Relationship | Rewrite of `BadDataNeighborOrientationCheck` in DREAM.3D v6.5.171. Worklist and Scanline preserve the neighbor rule with the documented defect corrections. | +| Oracle (confirmed) | Class 1 expected masks and Class 4 invariants are encoded in `test/BadDataNeighborOrientationCheckTest.cpp`. CI covers 33 ordinary test cases. All pass in CI. | +| Code paths enumerated | 19 of 29 exercised by the listed tests. Ten paths have no direct fixtures. | +| Tests today | 35 ordinary test cases and 2 hidden template variants cover masks, propagation, invariants, input validation and SIMPL conversion. | +| Exemplar archive | `bad_data_neighbor_orientation_check_v2.tar.gz` supplies test inputs. Expected masks are defined in the test source. | +| Legacy comparison | Run against DREAM.3D v6.5.171 on 2026-09-17: 12 of 28 cases match. A local proof version matches all 28 cases. | +| Bug flags | None. | +| V&V phase | COMPLETE. | ## Summary -`BadDataNeighborOrientationCheckFilter` iteratively flips "bad" voxels in a `Mask` array to "good" if a sufficient number of their same-phase face neighbors have crystallographically-similar orientations within the user-supplied misorientation tolerance. Verification used a **Class 1 (Analytical) hand-derived dataset of 27 algorithmic fixtures** with expected `Mask` outputs inline in the test source as `std::array` literals, plus a **Class 4 (Invariant) companion oracle** asserting monotonicity, no-degrade, and idempotence. A direct A/B against DREAM3D 6.5.171 confirms the SIMPLNX rewrite correctly fixes two legacy defects (D1, D2 — documented as Deviations) and a SIMPLNX-side float-π precision bug surfaced during this V&V cycle (tolerance computation promoted from `float` + `numbers::pi_v` to `double` + `numbers::pi_v` to remove a ~5e-9 rad amplification at the exact-tolerance boundary). All 31 tests pass bit-identical to the analytical oracle. +`BadDataNeighborOrientationCheckFilter` changes bad mask values to good when enough good face neighbors have the same phase and a misorientation below the tolerance. Class 1 expected masks and Class 4 invariants check the result. The five documented defects below are fixed. ## Algorithm Relationship -*Classification:* **Port** ~~| Minor changes | Rewrite | New filter~~ +*Classification:* Rewrite -*Evidence:* Near line-by-line translation of legacy `BadDataNeighborOrientationCheck::execute()`. Same SIMPL UUID retained; SIMPL 6.4/6.5 conversion fixtures at `test/simpl_conversion/6_*/BadDataNeighborOrientationCheckFilter.json`. Two-pass control flow and per-voxel misorientation logic are preserved (see Summary). - -*Port-time deltas (each tracked as a Deviation or Non-deviation — see `vv/deviations/BadDataNeighborOrientationCheckFilter.md`):* - -1. **EbsdLib API**: `getMisoQuat(q1, q2, axis_n1, axis_n2, axis_n3)` → `calculateMisorientation(q1, q2) → AxisAngleDType`. Modernized return type; mathematically equivalent in the absence of precision-sensitive boundary cases. -2. **Mask handling**: legacy unpacks `BoolArrayType` directly; SIMPLNX uses `MaskCompareUtilities::MaskCompare` to handle both `Bool` and `UInt8` mask backings transparently. UX-only, no behavioral delta. -3. **Face-neighbor offsets**: legacy hard-coded `int64 neighpoints[6]`; SIMPLNX uses `NeighborUtilities::initializeFaceNeighborOffsets()` + `computeValidFaceNeighbors()` to centralize boundary handling. PR #1590 made this 2D-aware (correctly skips +/-Z neighbors when `dims[2] == 1`). -4. **Progress reporting**: legacy direct `notifyStatusMessage`; SIMPLNX emits throttled progress feedback with stage info (`Level X of Y`). UX-only. -5. **`quat.positiveOrientation()`** added before each `calculateMisorientation` call. Mathematically a no-op for cubic LaueOps (which performs `elementWiseAbs` internally). No behavioral delta. -6. **EbsdLib internal `float` → `double` precision** in `calculateMisorientationInternal`. Modern API takes `QuatD`; legacy was `QuatF`. Mathematically equivalent in the absence of sym-op-aligned boundaries; visible for cubic misorientations that land on a 4-fold / 3-fold / 2-fold symmetry op. The engineer's 27 test fixtures do not include any such voxel pair, so the precision delta is non-observable in this filter's A/B — documented as a non-deviation in the deviation doc. -7. **Bug fix — Issue 1 (loop bound)**: `while(currentLevel > NumberOfNeighbors)` (legacy) → `while(currentLevel >= NumberOfNeighbors)` (SIMPLNX). Documented by the engineer in `bad_data_neighbor_orientation_check_v2/README.md` §"Issue 1". Confirmed by direct A/B against legacy 6.5.171. Tracked as **Deviation D1**. -8. **Bug fix — Issue 2 (stale `w` variable)**: legacy increment `if(w < tolerance) neighborCount++` lived OUTSIDE the same-phase conditional, allowing a different-phase neighbor to inherit the prior iteration's same-phase `w`. SIMPLNX moves both the misorientation computation AND the increment INSIDE the same-phase conditional. Tracked as **Deviation D2**. -9. **SIMPLNX-side tolerance precision fix**: tolerance computation promoted from `float` + `numbers::pi_v` to `double` + `numbers::pi_v` to remove a ~5e-9 rad amplification that caused 4 boundary-exact Case 1.X.3 fixtures to disagree with the analytical oracle. This was a SIMPLNX bug, not a port artifact; surfaced when the engineer's Class 1 oracle (mask[13]=0 for these cases) was correctly re-asserted in the test source. -10. **Algorithm review hardening**: cancel checking added at all loop levels; `getDataAs` replaced with `getDataRefAs`; CrystalStructures bounds-validation at `operator()` entry; defensive per-voxel `laueClassIndex` guard; tightened naming + comments. No behavioral delta. - -*Material PRs since baseline (2025-10-01):* - -- **PR #1499** — *"REV: Bad Data Neighbor Orientation Check"* (merged 2026-02-02) — **central V&V event.** Algorithm review pass + Issue 1 + Issue 2 fixes + comprehensive 28-case test rewrite. Engineer: Nathan Young. -- PR #1472 — EbsdLib 2.0.0 API bump (pinned dependency for this filter; effective EbsdLib pin at time of V&V completion is 2.4.1 commit `5c8c993`). -- PR #1523 — `NeighborUtilities` extracted as a shared module (no behavioral delta). -- PR #1538 — Test-sentinel infrastructure for tar.gz extraction (no behavioral delta). -- PR #1588 — SIMPL Backwards Compatibility test added. -- PR #1590 — `NeighborUtilities` 2D-aware path (`dims[2] == 1` correctly skips +/-Z neighbors). +*Evidence:* Worklist and Scanline replace the repeated full-volume scans in DREAM.3D v6.5.171. Both implementations retain the neighbor rule and correct the five defects listed below. ## Oracle -*Class:* **1 (Analytical)** primary + **4 (Invariant)** companion. Class 3 (Paper-based) N/A — this filter delegates misorientation math to `ebsdlib::LaueOps::calculateMisorientation`; the Rowenhorst 2015 paper-based verification of that math is part of EbsdLib's own V&V, not this filter's. - -### Applied (Class 1 — Analytical) - -Expected `Mask` outputs are derived in closed form from the input `Quats` + `Phases` + initial `Mask` + `(MisorientationTolerance, NumberOfNeighbors)` parameters by hand-tracing the algorithm: (a) pairwise misorientations between same-phase voxel pairs (closed-form for pure φ1-rotations); (b) initial per-voxel count of within-tolerance face neighbors; (c) iterative-decay walk that flips each masked-false voxel whose count meets `currentLevel`, decrementing `currentLevel` from 6 to `NumberOfNeighbors`. The engineer's `bad_data_neighbor_orientation_check_v2/test_design.md` bundles the derivation for every one of 27 algorithmic cases, with `Mask` / `Phases` / `Quats` input arrays and the expected output `Mask` array depicted in 3×3 (or 5×5) grid form per case. - -The Class 1 oracle's design choices that govern boundary behavior: - -| Configuration| Cases | Engineer's design intent | -|------------------------------------------------|--------------------------------------|-----------------------| -| **Pure φ1 rotations** `(φ1, 0, 0)` Bunge ZXZ | All 27 | Misorientation between any two voxels equals `|Δφ1|` modulo the c-axis symmetry of the Laue group — closed-form derivable. | -| **Strict `<` tolerance comparison** | All 27 | Misorientations that land at *exactly* the user-supplied tolerance are excluded. Case 1.X.3 (X ∈ {3,4,5,6}) deliberately places voxel pairs at exactly 5° to exercise this boundary semantic. | -| **Same-phase requirement** | Case 1.X.2 + Case 1.X.3 (mixed) | Different-phase neighbors are skipped regardless of their misorientation. Case 1.2.2 implicitly serves as the SIMPLNX-side regression test for D2 (legacy stale-`w` bug) — see deviation doc. | -| **Background voxels (phase ≤ 0) skip** | Implicit | A voxel whose phase resolves to the `UnknownCrystalStructure` sentinel is skipped (cellPhases > 0 guard). Allows valid use of the `999` sentinel that `CreateEnsembleInfo` prepends at index 0. | - -### Applied (Class 4 — Invariant) - -Two invariants every filter run must satisfy regardless of input configuration, asserted via `namespace ClassFourInvariants` in the test source: - -- **Monotonicity** — count of `Mask == true` voxels is non-decreasing across one filter run. -- **No-degrade** — no voxel goes from `true` (good) to `false` (bad). - -A third invariant (**Idempotence**: running the filter on its own output produces no further change) is asserted via a dedicated test using Case 4 input. - -### Encoded +*Class:* 1 (Analytical), with 4 (Invariant) companions. -- **Class 1 (Analytical)**: `test/BadDataNeighborOrientationCheckTest.cpp` — 27 `TEST_CASE` blocks (Case 1.1.1 through Case 4) each with an inline `std::array expectedMask` and per-voxel `REQUIRE(maskStore.getValue(i) == expectedMask[i])` checks. ~729 base-case assertions plus several thousand additional assertions for the 5×5×5 fixtures. -- **Class 4 (Invariant)**: `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Class 4 Invariants Sweep` — DYNAMIC_SECTIONs over all 18 Case 1.X.Y fixtures, asserting monotonicity + no-degrade via the `ClassFourInvariants::AssertInvariants` helper. -- **Class 4 (Idempotence)**: `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Class 4 Idempotence` — runs Case 4 input through the filter twice, asserting second-run mask equals first-run mask. -- **2D path coverage**: `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: 2D Image Fixture (3x3x1)` — inline-constructed 3×3×1 image with a single bad center voxel and 4 good face neighbors, NN=4, expected flip. Exercises the PR #1590 2D-aware `NeighborUtilities` path. -- *(kept)* `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: SIMPL Backwards Compatibility` — SIMPL 6.4 + 6.5 conversion paths via `DYNAMIC_SECTION`; UUID + argument-key + parameter-value validation only. +*Applied:* Expected masks follow the strict angle limit, same-phase rule and repeated neighbor propagation; invariant checks require no loss of good voxels and an unchanged result after a second run. -### Second-engineer review +*Encoded:* `src/Plugins/OrientationAnalysis/test/BadDataNeighborOrientationCheckTest.cpp` — `Case 1.1.1` through `Case 4` give 27 fixtures; `2D Image Fixture (3x3x1)`, `real HDF5 reverse-slice cascade oracle`, `Class 4 Invariants Sweep` and `Class 4 Idempotence` add analytical and invariant checks. CI covers 33 ordinary test cases. All pass in CI. -- MAJ reviewed all topics and agrees with their assesment. -- *The Class 1 hand-derivations in `test_design.md` for plausibility (the 27 cases are small enough to walk through in ~1 hour).* -- *The Class 4 invariant set for completeness — are there other properties this algorithm must satisfy?* -- *The Phase 9 deviation narrative (D1 loop bound + D2 stale `w`) and the determination that the EbsdLib 2.4.1 CubicOps precision improvement is non-observable in this filter's test data.* +*Second-engineer review:* Michael Jackson, 2026-06-01, as recorded in the sign-off. ## Bugs found and fixed -The existing D1 loop-bound and D2 stale-angle corrections remain in this branch. Their historical affected behavior is documented in `vv/deviations/BadDataNeighborOrientationCheckFilter.md`. No production algorithm changed during this OOC recertification. +All defects in this table are fixed. The fixes will be in the DREAM3D-NX release after v7.4.2. DREAM3D-NX v7.4.1 contains the fix for `BadDataNeighborOrientationCheckFilter-D1`. DREAM3D-NX v7.4.1 contains the fix for `BadDataNeighborOrientationCheckFilter-D2`. DREAM3D-NX v7.4.2 contains the fix for `BadDataNeighborOrientationCheckFilter-D3`. DREAM3D-NX v7.4.2 contains the fix for `BadDataNeighborOrientationCheckFilter-D4`. -## Code path coverage - -9 of 11 paths exercised. - -Source: `Algorithms/BadDataNeighborOrientationCheck.cpp` (37 lines), `BadDataNeighborOrientationCheckWorklist.cpp` (243 lines), and `BadDataNeighborOrientationCheckScanline.cpp` (376 lines). +| Deviation | Defect | Affected released versions | Resolution | +|-----------|--------|----------------------------|------------| +| `BadDataNeighborOrientationCheckFilter-D1` | The convergence loop omitted the requested minimum level, so eligible bad voxels stayed bad. | DREAM3D-NX v7.0.0 through v7.4.0. DREAM.3D v6.5.171 was affected. | Both implementations include `currentLevel == NumberOfNeighbors`. | +| `BadDataNeighborOrientationCheckFilter-D2` | An angle from a previous neighbor could cause a different-phase neighbor to count as a match. | DREAM3D-NX v7.0.0 through v7.4.0. DREAM.3D v6.5.171 was affected. | Both implementations test the angle inside the same-phase condition. | +| `BadDataNeighborOrientationCheckFilter-D3` | Float conversion of the angle tolerance could admit a neighbor at the strict boundary. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | The filter converts degrees to radians with double precision. | +| `BadDataNeighborOrientationCheckFilter-D4` | Invalid crystal-structure values could index outside the orientation-operator array. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | The filter rejects invalid Laue indices with `-54901` and skips the supported unknown-structure sentinel. | +| `BadDataNeighborOrientationCheckFilter-D5` | An invalid phase on a bad voxel could index outside Crystal Structures. | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was affected. | The filter skips nonpositive phases and returns `-54902` for a participating phase beyond the ensemble array. | -The algorithm has two passes: (a) initial face-neighbor count over all voxels, and (b) iterative-decay flip pass that decrements `currentLevel = 6 → NumberOfNeighbors`. Each pass's per-voxel kernel has branches for mask state, phase match, and tolerance pass. +## Code path coverage -| # | Pass | Path | Test case | -|---|--------------------|----------------------------------------|---------------| -| 1 | (a) Initial scan | Voxel mask = true → skip | All cases — every fixture has a mix of true/false voxels | -| 2 | (a) Initial scan | Mask = false, neighbor in different phase or unphased (`cellPhases[voxelIndex] > 0` guard) → skip neighbor | `Case 1.X.2` (3-phase invalid) + Case 4 (mixed phases) | -| 3 | (a) Initial scan | Mask = false, neighbor on out-of-bounds face (corner / edge / 2D image +/-Z) → skip | All 3×3×3 cases (corners + edges) + `2D Image Fixture` (Z bounds) | -| 4 | (a) Initial scan | Mask = false, neighbor same-phase + misorientation `< tolerance` → increment `neighborCount`| All cases — primary algorithmic path | -| 5 | (a) Initial scan | Mask = false, neighbor same-phase + misorientation `>= tolerance` → don't increment | `Case 1.X.3` (boundary-exact at 5°) + `Case 1.1.3` (6° vs 1° = ~5°+ε) | -| 6 | (b) Iterative flip | `neighborCount[voxelIndex] >= currentLevel` AND mask still false → flip + update still-bad neighbors' counts | `Case 1.X.1` (basic flip), `Case 2.X` (sequential), `Case 3.X` (long chains), Case 4 (semi-complex) | -| 7 | (b) Iterative flip | Defensive `laueClassIndex >= numOrientationOps` skip (sentinel-aware bounds guard) | *Not directly tested.* Exercised implicitly when the filter runs on any fixture whose CrystalStructures contains the `UnknownCrystalStructure` sentinel at an unused index (all 27 base fixtures). Low-value gap — adding a deliberate sentinel-at-used-index fixture would only verify the early-exit branch. | -| 8 | Scanline | Reverse propagation crosses five rolling slices; Bool and UInt8 bulk mask paths | `real HDF5 reverse-slice cascade oracle` — exact full mask and target-only OOC dispatch counter | -| 9 | Validation | Participating phase bounds | `Phase Index Bounds` | -| 10 | Preflight | Cell tuple-count mismatch | `Preflight Error - Cell array tuple count mismatch (-6809)` — asserts preflight invalid | -| 11 | Cancellation | Stop an active propagation pass | *Not directly tested. Requires cancel-signal injection and is outside this scope.* | +19 of 29 paths exercised by the listed tests. The table maps source paths to existing fixtures and gives the reason for each of the ten paths without a fixture. + +Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/BadDataNeighborOrientationCheck.cpp` (37 lines), `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/BadDataNeighborOrientationCheckWorklist.cpp` (261 lines), `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/BadDataNeighborOrientationCheckScanline.cpp` (381 lines), and `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/BadDataNeighborOrientationCheckFilter.cpp` (187 lines). + +Worklist counts matching neighbors and propagates changes through a queue. Scanline recounts neighbors in rolling slices until each level is stable. + +| # | Phase | Path | Test case | +|---|-------|------|-----------| +| 1 | Neighbor check | Mask is already good → skip | `Case 1.1.1`; `2D Image Fixture (3x3x1)` | +| 2 | Neighbor check | Neighbor has a different phase → do not count | `Case 1.2.2`; `Case 4` | +| 3 | Neighbor check | Face lies outside the image, including Z faces in a one-slice image → skip | `2D Image Fixture (3x3x1)`; `real HDF5 reverse-slice cascade oracle` | +| 4 | Neighbor check | Good neighbor has the same phase and angle below tolerance → count | `Case 1.1.1`; `2D Image Fixture (3x3x1)` | +| 5 | Neighbor check | Angle is at or above tolerance → do not count | `Case 1.3.3`; `real HDF5 reverse-slice cascade oracle` | +| 6 | Propagation | Count reaches the current level → flip and propagate | `Case 2.1` through `Case 2.6`; `Case 3.1`; `Case 3.2` | +| 7 | Neighbor check | Positive phase resolves to the unknown-structure sentinel → skip | *Not directly tested. No fixture assigns a participating positive phase to the sentinel.* | +| 8 | Scanline | Reverse propagation crosses five slices and repeats passes | `real HDF5 reverse-slice cascade oracle` — UInt8 and Bool variants assert all 45 mask values | +| 9 | Validation | Participating phase exceeds the ensemble bounds → error `-54902` | `Phase Index Bounds` — bad center voxel variant | +| 10 | Preflight | Cell tuple counts differ → error `-6809` | `Preflight Error - Cell array tuple count mismatch (-6809)` | +| 11 | Cancellation | Stop during Worklist initialization, queue construction, propagation or between levels; stop during a Scanline slice pass | *Not directly tested. No fixture injects a cancellation signal.* | +| 12 | Dispatch | Select Worklist on arrays in memory | `Case 1.1.1` through `Case 4` — algorithm scenario harness and exact masks | +| 13 | Dispatch | Select Scanline on arrays in memory or HDF5 stores | `Case 1.1.1` through `Case 4` — scenario harness; `real HDF5 reverse-slice cascade oracle` — dispatch counter | +| 14 | Neighbor check | Bad voxel has phase zero → skip | `real HDF5 reverse-slice cascade oracle` — background remains bad | +| 15 | Neighbor check | Bad voxel has a negative phase → skip | *Not directly tested. Fixtures do not assign negative phases.* | +| 16 | Neighbor check | Neighbor is bad → do not count it as good | `2D Image Fixture (3x3x1)` — corners stay bad | +| 17 | Propagation | Count is below the current level → leave the mask unchanged | `Case 1.3.2`; `2D Image Fixture (3x3x1)` | +| 18 | Validation | Invalid phase belongs to an already good voxel → skip it | `Phase Index Bounds` — good corner voxel variant checks successful execution only | +| 19 | Validation | Mask cannot be loaded → error `-54900` | *Not directly tested. This requires a direct algorithm call that bypasses parameter validation.* | +| 20 | Validation | Crystal structure is outside the Laue range and is not the sentinel → error `-54901` | *Not directly tested. No fixture supplies an invalid non-sentinel crystal structure.* | +| 21 | Worklist | Discard a duplicate queue entry or an entry below the current level | *Not directly tested. No fixture isolates the queue guard.* | +| 22 | Worklist | Recheck phase and Laue bounds after a flip | *Not directly tested. Preflight restricts the threshold to 1 through 6. No fixture bypasses preflight to isolate these guards.* | +| 23 | Scanline | Read and write UInt8 mask slices | `real HDF5 reverse-slice cascade oracle` — UInt8 variant | +| 24 | Scanline | Convert Bool mask slices for bulk reads and writes | `real HDF5 reverse-slice cascade oracle` — Bool variant | +| 25 | Scanline | Return an ensemble, quaternion, phase or mask I/O error | *Not directly tested. No fixture injects a store read or write failure.* | +| 26 | Scanline | Use fallback per-element mask I/O | *Not directly tested. Accepted masks use the UInt8 or Bool bulk paths.* | +| 27 | Propagation | Requested level exceeds six → no propagation | *Not directly tested. Preflight rejects a neighbor requirement above six. No fixture bypasses preflight to isolate this loop guard.* | +| 28 | Preflight | Neighbor requirement is outside 1 through 6 → error `-6810` | `Preflight Error - Number of neighbors out of range (-6810)` — values 7, 0 and -1 | +| 29 | Preflight | Quaternions, phases and mask have equal tuple counts that differ from the selected geometry's cell count → error `-6810` | `Geometry cell tuple counts` — all three arrays have 4 or 12 tuples for 8 cells | ## Test inventory -| Test case | Status | Notes | -|-------------------|-------------|-----------------------------------------------| -| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.1.1` through `Case 1.6.3` (18 cases) | kept | Class 1 hand-derived `expectedMask` per case, 27-element arrays. The 4 cases 1.X.3 (X ∈ {3,4,5,6}) were reverted from a 2026-05-29 circular-oracle update back to the engineer's hand-derived values during Phase 6. | -| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 2.1` through `Case 2.6` (6 cases) | kept | 5×5×5 sequential / recursive fixtures. Expected output is `all 1` (full convergence), asserted via `maskStore.getValue(i) != 1` loop. | -| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 3.1` + `Case 3.2` | kept | 5×5×5 long-chain cases with `NumberOfNeighbors = 1`; verifies full-grid convergence. | -| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 4` | kept | 5×5×5 semi-complex fixture with 3 phases, `NumberOfNeighbors = 4`. Hand-derived 125-element expected mask. | -| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Class 4 Invariants Sweep` | new-for-V&V | Added 2026-05-29. DYNAMIC_SECTIONs over all 18 Case 1.X.Y fixtures. Asserts monotonicity + no-degrade per filter run. | -| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Class 4 Idempotence` | new-for-V&V | Added 2026-05-29. Runs Case 4 input through the filter twice; asserts second run reproduces first run exactly. | -| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: 2D Image Fixture (3x3x1)` | new-for-V&V | Added 2026-05-29. Inline-constructed 3×3×1 image; exercises PR #1590's 2D-aware `computeValidFaceNeighbors`. Does not consume the exemplar archive. | -| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: SIMPL Backwards Compatibility` | kept | Added by PR #1588. `DYNAMIC_SECTION` over SIMPL 6.4 + 6.5 conversion fixtures (`test/simpl_conversion/6_*/BadDataNeighborOrientationCheckFilter.json`); validates UUID + argument-key + parameter-value decoding. | -| `Preflight Error - Cell array tuple count mismatch (-6809)` | kept | Rejects unequal cell tuple counts. | -| `Phase Index Bounds` | kept | Retains the branch's invalid-phase regression. | -| `real HDF5 reverse-slice cascade oracle - uint8` | new-for-V&V | Five 3×3 slices; one last-slice seed propagates backward through the central column, while a 30° control stays bad. | -| `real HDF5 reverse-slice cascade oracle - bool` | new-for-V&V | Same literal mask oracle through the Bool bulk conversion path. | - -On 2026-09-21, serial CTest passed 33/33 in both DREAM3DNX builds. The two hidden real-HDF5 cases passed 139 assertions. They assert all four input stores and the modified mask use HDF5-OOC and prove the target selected Scanline on OOC arrays. Each output mask has exactly the five central-column voxels set. This is an algorithm slice-buffer boundary witness, not a physical chunk-eviction benchmark. Existing upstream/develop oracle assertions and tolerances remain unchanged. D1 and D2 dispositions are retained; no fresh legacy binary comparison is claimed. +| Test case | Status | Notes | +|-----------|--------|-------| +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.1.1` | kept | Class 1. Neighbor requirement 1; compares all 27 mask values with a literal array. | +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.1.2` | kept | Class 1. Neighbor requirement 1; compares all 27 mask values with a literal array. | +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.1.3` | kept | Class 1. Neighbor requirement 1; compares all 27 mask values with a literal array. | +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.2.1` | kept | Class 1. Neighbor requirement 2; compares all 27 mask values with a literal array. | +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.2.2` | kept | Class 1. Neighbor requirement 2; compares all 27 mask values with a literal array. | +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.2.3` | kept | Class 1. Neighbor requirement 2; compares all 27 mask values with a literal array. | +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.3.1` | kept | Class 1. Neighbor requirement 3; compares all 27 mask values with a literal array. | +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.3.2` | kept | Class 1. Neighbor requirement 3; compares all 27 mask values with a literal array. | +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.3.3` | kept | Class 1. Neighbor requirement 3; compares all 27 mask values with a literal array. Center stays bad at the tolerance boundary (BadDataNeighborOrientationCheckFilter-D3). | +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.4.1` | kept | Class 1. Neighbor requirement 4; compares all 27 mask values with a literal array. | +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.4.2` | kept | Class 1. Neighbor requirement 4; compares all 27 mask values with a literal array. | +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.4.3` | kept | Class 1. Neighbor requirement 4; compares all 27 mask values with a literal array. Center stays bad at the tolerance boundary (BadDataNeighborOrientationCheckFilter-D3). | +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.5.1` | kept | Class 1. Neighbor requirement 5; compares all 27 mask values with a literal array. | +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.5.2` | kept | Class 1. Neighbor requirement 5; compares all 27 mask values with a literal array. | +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.5.3` | kept | Class 1. Neighbor requirement 5; compares all 27 mask values with a literal array. Center stays bad at the tolerance boundary (BadDataNeighborOrientationCheckFilter-D3). | +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.6.1` | kept | Class 1. Neighbor requirement 6; compares all 27 mask values with a literal array. | +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.6.2` | kept | Class 1. Neighbor requirement 6; compares all 27 mask values with a literal array. | +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.6.3` | kept | Class 1. Neighbor requirement 6; compares all 27 mask values with a literal array. Center stays bad at the tolerance boundary (BadDataNeighborOrientationCheckFilter-D3). | +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 2.1` | kept | Class 1. Neighbor requirement 5; requires every mask value to be 1 after propagation. | +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 2.2` | kept | Class 1. Neighbor requirement 5; requires every mask value to be 1 after propagation. | +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 2.3` | kept | Class 1. Neighbor requirement 5; requires every mask value to be 1 after propagation. | +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 2.4` | kept | Class 1. Neighbor requirement 5; requires every mask value to be 1 after propagation. | +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 2.5` | kept | Class 1. Neighbor requirement 5; requires every mask value to be 1 after propagation. | +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 2.6` | kept | Class 1. Neighbor requirement 5; requires every mask value to be 1 after propagation. | +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 3.1` | kept | Class 1. Neighbor requirement 1; requires every mask value to be 1 after propagation. | +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 3.2` | kept | Class 1. Neighbor requirement 1; requires every mask value to be 1 after propagation. | +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 4` | kept | Class 1. Neighbor requirement 4; compares all 125 mask values with a literal array. | +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Preflight Error - Cell array tuple count mismatch (-6809)` | kept | Checks preflight error `-6809` for 9 phase tuples and 10 mask/quaternion tuples. | +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Preflight Error - Number of neighbors out of range (-6810)` | kept | Checks preflight error `-6810` for neighbor requirements 7, 0 and -1. | +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: SIMPL Backwards Compatibility` | kept | Checks UUID, comments, scalar values and array paths in `SIMPL 6.5 (UUID)` and `SIMPL 6.4 (Filter_Name)` dynamic sections. | +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Class 4 Invariants Sweep` | new-for-V&V | Checks no loss of good voxels and a nondecreasing good-voxel count in 18 dynamic sections, one for each Case 1 fixture. | +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Class 4 Idempotence` | new-for-V&V | Runs Case 4 twice; compares every value in the second mask with the first. | +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: 2D Image Fixture (3x3x1)` | new-for-V&V | Compares all 9 values with `[0,1,0,1,1,1,0,1,0]`; the center flips and the corners remain bad. | +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Phase Index Bounds` | kept | Checks error `-54902` for an invalid phase on the bad center and success when it is on a good corner (BadDataNeighborOrientationCheckFilter-D5). | +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Geometry cell tuple counts` | new-for-V&V | Checks `-6810` when quaternions, phases and mask each have 4 or 12 tuples for 8 cells. Checks `-6809` when only one array has 4 tuples. Checks diagnostic paths and counts, and accepts 8 tuples per array outside Cell Data. | +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: real HDF5 reverse-slice cascade oracle` — `uint8` template variant | new-for-V&V | Hidden. Requires HDF5-OOC input stores and Scanline dispatch; compares all 45 mask values with the central-column oracle. A 30° control stays bad. | +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: real HDF5 reverse-slice cascade oracle` — `bool` template variant | new-for-V&V | Hidden. Requires HDF5-OOC input stores and Scanline dispatch; compares all 45 mask values with the central-column oracle. A 30° control stays bad. | ## Exemplar archive - **Archive:** `bad_data_neighbor_orientation_check_v2.tar.gz` -- **SHA512:** `6452cfb1f2394c10050082256f60a2068cfad78ef742e9e35b1d6e63b3fb7c35c9fe7bbe093bed4dbb4e758c49ec6da7b1f7e2473838a0421f39fbdd9f4a2f76` +- **SHA512:** `c311d636f56027da8f3b665375005230be83bb9060aed29dd1aada928d7afbce89d7be845626139c19025a89aaf1ac52b099c8efb8b99f246fc0bfad3c4ce128` - **Provenance:** `src/Plugins/OrientationAnalysis/vv/provenance/BadDataNeighborOrientationCheckFilter.md` -Archive contents: **INPUT** `.dream3d` files only — one per algorithmic test case, organized as `case_X/case_X_Y/case_X_Y_Z/case_X_Y_Z_input.dream3d` (3×3×3 fixtures) and `case_X/case_X_Y/case_X_Y_input.dream3d` (5×5×5 fixtures). Expected output `Mask` arrays are hard-coded inline in `test/BadDataNeighborOrientationCheckTest.cpp`. The archive's local copy also bundles the engineer's `README.md` (legacy bug documentation) and `test_design.md` (Class 1 oracle source-of-truth). No archive re-bundling was needed during this V&V cycle. - -## Deviations from DREAM3D 6.5.171 - -Two documented deviations, both legacy defects fixed by the SIMPLNX rewrite. Three further behaviors common to both implementations are explicitly captured as non-deviations to prevent future re-discovery. - -### BadDataNeighborOrientationCheckFilter-D1 - -- **Symptom:** 6.5.171 fails to flip a bad voxel whose good-neighbor count is exactly equal to `NumberOfNeighbors`. SIMPLNX correctly flips. Observable in 15 of the 27 V&V fixtures (288 mask bytes total). -- **Root cause:** Bug in 6.5.171. Legacy convergence loop `while(currentLevel > m_NumberOfNeighbors)` walks `currentLevel` from 6 down to `N + 1` and never executes the `currentLevel == N` iteration. SIMPLNX corrects to `>=`. -- See `vv/deviations/BadDataNeighborOrientationCheckFilter.md`. - -### BadDataNeighborOrientationCheckFilter-D2 - -- **Symptom (latent):** 6.5.171 can count a different-phase neighbor's misorientation as within tolerance if a *previous* same-phase neighbor's `w` was small, because the legacy threshold check sits outside the same-phase conditional and inherits stale `w`. Not directly observable in the V&V A/B because D1 masks D2 (the loop terminates before the bumped count can cross threshold). Real and code-evident. -- **Root cause:** Bug in 6.5.171. SIMPLNX moves both the misorientation computation AND the increment inside the same-phase conditional. Case 1.2.2 implicitly serves as the SIMPLNX-side regression coverage. -- See `vv/deviations/BadDataNeighborOrientationCheckFilter.md`. - -### Non-deviations (documented for future-engineer awareness) - -- **EbsdLib 2.4.1 CubicOps precision improvement** — `2·atan2(|v|, w)` vs `acos(w)` for cubic sym-op-aligned misorientations. Real precision improvement; not observed in this filter's test data because no engineer-supplied voxel pair lands on a cubic sym op. -- **Raster-order flood-fill** — both filters iterate voxels in linear order with immediate neighbor-count updates after each flip; the final mask depends on linear scan order. Algorithm characteristic, not a defect. -- **Mixed-phase neighbor rejection** — both filters require `cellPhases[voxelIndex] == cellPhases[neighborPoint]` AND `cellPhases[voxelIndex] > 0`. Algorithm characteristic. +## Deviations from DREAM.3D v6.5.171 -### Downstream impact note +See `vv/deviations/BadDataNeighborOrientationCheckFilter.md` for the root cause, affected users, and recommendation for each deviation. The comparison on 2026-09-17 covers the 27 analytical fixtures and Small IN100. Twelve cases match; 16 differ in Mask. Phases, Quats and Crystal Structures match in all 28 cases. -D1 and D2 propagate through any downstream filter that consumes this filter's `Mask` output. Users coming from DREAM3D 6.5.171 may see materially different reconstructions on data where the canonical Small IN100 pipeline is run with the default `NumberOfNeighbors = 4` — SIMPLNX flips more voxels than 6.5.171 did, producing fuller grain reconstructions and smoother grain boundaries. **Trust SIMPLNX.** +| Deviation | Observed difference | +|-----------|---------------------| +| `BadDataNeighborOrientationCheckFilter-D1` | SIMPLNX includes the requested minimum neighbor level; DREAM.3D v6.5.171 omits it. | +| `BadDataNeighborOrientationCheckFilter-D2` | SIMPLNX rejects different-phase neighbors without reusing an earlier angle; DREAM.3D v6.5.171 permits stale-angle counts. A local proof version isolates the phase-gating correction. | diff --git a/src/Plugins/OrientationAnalysis/vv/CAxisSegmentFeaturesFilter.md b/src/Plugins/OrientationAnalysis/vv/CAxisSegmentFeaturesFilter.md index 1f436ae5b9..0695a13cba 100644 --- a/src/Plugins/OrientationAnalysis/vv/CAxisSegmentFeaturesFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/CAxisSegmentFeaturesFilter.md @@ -1,137 +1,164 @@ # V&V Report: CAxisSegmentFeaturesFilter -| | | -|-----------|--------------------------| -| Plugin | OrientationAnalysis | -| SIMPLNX UUID | `9fe07e17-aef1-4bf1-834c-d3a73dafc27d` | -| SIMPLNX Human Name | Segment Features (C-Axis Misalignment) | -| DREAM3D 6.5.171 equivalent | `CAxisSegmentFeatures` — `Source/Plugins/Reconstruction/ReconstructionFilters/CAxisSegmentFeatures.{h,cpp}` (SIMPL UUID `bff6be19-1219-5876-8838-1574ad29d965`) | -| Verified commit | **| -| Status | COMPLETE | -| Sign-off | Michael A. Jackson — 2026-07-23. Second engineer: Matthew Marine, 2026-07-25 (PR #1685 review). | +| | | +|--------|--------------| +| Plugin | OrientationAnalysis | +| SIMPLNX UUID | `9fe07e17-aef1-4bf1-834c-d3a73dafc27d` | +| SIMPLNX Human Name | Segment Features (C-Axis Misalignment) | +| DREAM.3D v6.5.171 equivalent | `CAxisSegmentFeatures` (Reconstruction plugin, SIMPL UUID `bff6be19-1219-5876-8838-1574ad29d965`) | +| Verified commit | ** | +| Status | COMPLETE | +| Sign-off | Michael Jackson, 2026-07-23; second engineer: Matthew Marine, 2026-07-25 | ## At a glance -| Aspect | Current state | -|------------------------|--------------------------| -| Algorithm Relationship | **Minor changes.** Line-for-line port of legacy `getSeed`/`determineGrouping` (same c-axis math, including the clamped `acos` and the π-fold), plus deliberate NX additions: crystal-structure validation, 26-neighbor scheme, uint8 masks, RectGrid input, deterministic opt-in FeatureId randomization. Three SIMPLNX defects (D1, D4, D5) are **resolved**. | -| Oracle (confirmed) | **Class 1 (Analytical) primary + Class 4 (Invariant) companion** — pure-Phi Bunge quats make the folded c-axis distance between cells exactly `min(\|ΔΦ\|, 180°−\|ΔΦ\|)`; expected FeatureIds derive in closed form. Encoded as 10 fixture tests in `test/CAxisSegmentFeaturesTest.cpp`; all pass. | -| Code paths enumerated | 20 of 20 enumerated; 18 exercised (2 gaps: defensive mask-instantiation error, cancel-signal path). | -| Tests today | 20 test cases: 8 Class 1 analytical (chain, π-fold, neighbor-scheme, mask bool/uint8, phase separation, 3D linearization, Quats-outside-cell-AM, RectGrid), 2 Class 4 invariants (randomize non-identity/determinism, masked-zero preservation), 2 exemption pins (phase-0, masked non-hex), 3 execute-error, 4 preflight-error, 1 SIMPL 6.4/6.5 conversion (DYNAMIC_SECTION). | -| Exemplar archive | **None — fixtures inlined in the test source.** The filter's consumption of `segment_features_test_data.tar.gz` (circular oracle) is retired; the archive remains for the EBSD segmentation tests. | -| Legacy comparison | **Run** (2026-07-22, rerun with TC5_3D 2026-07-24, `vv/comparisons/CAxisSegmentFeaturesFilter/`) — all 5 shared-behavior fixtures (incl. a 3×2×2 masked fixture covering the y/z stride branches) match 6.5.171 at the segmentation-partition level with identical feature counts; bit-identical ids are unattainable because 6.5.171 always clock-randomizes FeatureIds (D2). | -| Bug flags | D1, D4, D5 — all SIMPLNX defects, all **resolved** and pinned by tests. No legacy bug flags.| -| V&V phase | Discovery, relationship, oracle, reconciliation, algorithm review, tests (dual-build), legacy comparison, deviations, provenance, docs — complete. Second-engineer review completed at PR per sign-off convention (Matthew Marine, 2026-07-25, PR #1685).| +| Aspect | Current state | +|--------|---------------| +| Algorithm Relationship | Rewrite of `CAxisSegmentFeatures` in DREAM.3D v6.5.171. Connected-component labeling replaces flood fill and preserves the folded c-axis angle rule. | +| Oracle (confirmed) | Class 1 analytical labels and Class 4 invariants are encoded in ten dedicated cases in `test/CAxisSegmentFeaturesTest.cpp`. All pass in CI. | +| Code paths enumerated | 31 of 44 exercised. Defensive guards, cancellation, external-store selection and injected failures have no direct fixtures. | +| Tests today | 29 test cases: 28 ordinary cases and one hidden input generator. They cover analytical labels, invariants, archive regression, validation and SIMPL conversion. | +| Exemplar archive | `segment_features_test_data.tar.gz` supplies inputs and four label arrays for regression tests. Independent expected values are defined in C++ fixtures. | +| Legacy comparison | Not run for the current algorithm. The DREAM.3D v6.5.171 comparison dated 2026-09-17 used DREAM3D-NX v7.4.2 and predates the rewrite. | +| Bug flags | None | +| V&V phase | COMPLETE | ## Summary -`CAxisSegmentFeaturesFilter` flood-fills neighboring cells into features when their crystallographic c-axes (hexagonal [0001] directions) are misaligned by less than a user tolerance. Verification used a Class 1 analytical oracle (pure-Phi Bunge fixtures with closed-form expected segmentations) plus Class 4 invariants; reconciliation exposed and fixed three SIMPLNX defects (unvalidated first seed in the shared driver, spurious rejection of unindexed/masked cells, RectGrid crash). Post-fix SIMPLNX matches the oracle exactly and matches DREAM3D 6.5.171 at the partition level on every shared code path; 5 deviations are documented, 3 of them fixed-this-cycle SIMPLNX bugs. +`CAxisSegmentFeaturesFilter` groups adjacent cells of the same positive phase when their folded c-axis angle is at or below the tolerance. Class 1 fixtures check exact labels, and Class 4 invariants check feature counts, background values and randomization. The seven defects listed below are fixed. ## Algorithm Relationship -*Classification:* **Minor changes** +*Classification:* Rewrite -*Evidence:* SIMPL UUID `bff6be19-1219-5876-8838-1574ad29d965` is preserved via `FromSIMPLJson` (see `test/simpl_conversion/6_*/CAxisSegmentFeaturesFilter.json`). `getSeed()` and `determineGrouping()` are line-for-line translations of the legacy methods (`CAxisSegmentFeatures.cpp` in the Reconstruction plugin), with the flood-fill driver hoisted into the shared `src/simplnx/Utilities/SegmentFeatures.cpp` (legacy used a `SegmentFeatures` base class the same way). +*Evidence:* Shared connected-component labeling replaces the legacy flood fill; `areNeighborsSimilar()` retains the normalized c-axis dot product, clamped angle and antiparallel fold. -*Port-time deltas:* +## Oracle -1. **Math API.** Legacy `FOrientTransformsType::qu2om` + `MatrixMath` → NX `ebsdlib::Quaternion::toOrientationMatrix()` + Eigen. Same arithmetic (quat → OM, transpose, ·[0,0,1], normalize, clamped `acos`, `w ≤ tol || π−w ≤ tol`); the A/B run shows no observable difference. -2. **Crystal-structure validation added** (D3). Legacy computes the [001] misalignment for any Laue class; NX requires participating cells to be Hexagonal_High/Low (errors `-8363`/`-8364`). Deliberate correctness guard; changes behavior only for inputs whose legacy output was meaningless. -3. **FeatureId randomization exposed and made deterministic** (D2). Legacy always randomizes with a clock seed (not a parameter); NX exposes `Randomize Feature Ids` (default false) with a fixed-seed shuffle. -4. **26-neighbor "All Connected" scheme added** (NX-only option; default remains the legacy 6-face behavior). -5. **RectGrid geometry and uint8 masks accepted** (NX-only capability; legacy is Image + bool-mask only). -6. **D1 (resolved):** PR #1466's driver restructure made the first seed an unvalidated raw index 0 — phantom/misgrown first feature when voxel 0 could not seed. Restored `getSeed()` for the first seed (also fixes EBSD/Scalar segmentation, which share the driver). -7. **D4 (resolved):** the delta-2 validation loop originally checked phase-0 and masked-out cells (which can never participate) and indexed `CrystalStructures` without a bounds check. -8. **D5 (resolved):** stale `getDataAs` cast crashed RectGrid input; now `IGridGeometry`, matching the sibling algorithms. -9. **Review hardening (no legacy behavior change):** preflight now rejects cell arrays whose tuple count differs from the geometry's cell count (`-652`, previously an out-of-bounds walk) and a cell AttributeMatrix smaller than the geometry (`-653`); `executeImpl` derives the FeatureIds path from the geometry's cell AttributeMatrix exactly as preflight does (previously from the Quats array's parent — a crash when Quats lived elsewhere) and creates it with that AttributeMatrix's tuple shape; a canceled run returns cleanly instead of misreporting `-87000`. +*Class:* 1 (Analytical), with 4 (Invariant) companions. -*Material PRs since baseline:* #1373 (26-neighbor option), #1444/#1498 (progress messaging), #1466 (feature-count fix; introduced D1), #1472 (EbsdLib 2.0 API), #1535 (preflight cleanup), #1501 (Vec3 consolidation). +*Applied:* Pure-Phi quaternions give folded distances `min(abs(ΔPhi), 180° − abs(ΔPhi))`; adjacency and equal positive phases determine exact labels, with N + 1 Active tuples and background zero. -## Oracle +*Encoded:* `src/Plugins/OrientationAnalysis/test/CAxisSegmentFeaturesTest.cpp::OrientationAnalysis::CAxisSegmentFeaturesFilter: Class 1 Analytical (Pure-Phi Chain, Face)` and seven analytical companions, plus two `Class 4 Invariants` cases. All pass in CI. -*Class:* **1 (Analytical)** primary + **4 (Invariant)** companion. +*Second-engineer review:* Matthew Marine, 2026-07-25. -*Applied:* For pure Bunge Euler angles (φ1=0, Φ, φ2=0), stored as quats `{sin(Φ/2), 0, 0, cos(Φ/2)}`, the sample-frame c-axis of a cell is `(0, ±sin Φ, cos Φ)`, so the c-axis angle between two cells is exactly `|ΦA−ΦB|` and the algorithm's folded metric is `min(|ΔΦ|, 180°−|ΔΦ|)`. Expected FeatureIds follow in closed form from per-cell Φ, the tolerance, and grid adjacency (derivation comment at `AnalyticalFixtures::QuatFromPhiDeg`). Class 4 companions: feature AM has `numFeatures+1` tuples, `Active[0]==0`, all real features active, masked/unindexed cells keep id 0, randomization preserves the partition and is a deterministic permutation of `{1..N}`. +## Bugs found and fixed -*Encoded:* `test/CAxisSegmentFeaturesTest.cpp` — `Class 1 Analytical (Pure-Phi Chain, Face)`, `(Pi-Fold Antiparallel C-Axes)`, `(Neighbor Scheme Face vs All)` [2 sections], `(Mask Excludes Voxel 0)` [2 sections], `(Phase Separation)`, `(3D Linearization, 3x2x2)`, `(Quats Outside Cell AttributeMatrix)`, `(RectGrid Geometry)`, `Class 4 Invariants (RandomizeFeatureIds)`, `Class 4 Invariants (RandomizeFeatureIds Preserves Masked Zeros)` — all pass in both builds. +All defects in this table are fixed. The fixes will be in the DREAM3D-NX release after v7.4.1. DREAM3D-NX v7.4.1 contains the fix for `CAxisSegmentFeaturesFilter-D6`. DREAM3D-NX v7.4.2 contains the fix for `CAxisSegmentFeaturesFilter-D1`. DREAM3D-NX v7.4.2 contains the fix for `CAxisSegmentFeaturesFilter-D4`. DREAM3D-NX v7.4.2 contains the fix for `CAxisSegmentFeaturesFilter-D5`. DREAM3D-NX v7.4.2 contains the fix for `CAxisSegmentFeaturesFilter-D7`. DREAM3D-NX v7.4.2 contains the fix for `CAxisSegmentFeaturesFilter-D8`. DREAM3D-NX v7.4.2 contains the fix for `CAxisSegmentFeaturesFilter-D9`. -*Second-engineer review:* **Signed off by Matthew Marine, 2026-07-25** (PR #1685 review, per sign-off convention). The pure-Phi c-axis derivation is sibling-shared with the previously reviewed `GroupMicroTextureRegionsFilter` / `ComputeFeatureNeighborCAxisMisalignmentsFilter` Class 1 oracles. The V&V work was authored by Michael A. Jackson, so the review is independent of the author. +| Deviation | Defect | Affected released versions | Resolution | +|-----------|--------|----------------------------|------------| +| `CAxisSegmentFeaturesFilter-D1` | An unchecked first seed could create a phantom feature or grow from a masked cell. | DREAM3D-NX v7.4.1. DREAM.3D v6.5.171 was not affected. | Labels only eligible cells. | +| `CAxisSegmentFeaturesFilter-D4` | Validation rejected excluded phases; later code also read Crystal Structures through unchecked phase indices. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | Validates only participating phases and returns `-8364` for an invalid positive index. | +| `CAxisSegmentFeaturesFilter-D5` | A stale ImageGeom cast crashed accepted RectGrid input. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | Accesses both geometries through `IGridGeometry`. | +| `CAxisSegmentFeaturesFilter-D6` | The feature count included the next unused ID, creating an extra empty tuple. | DREAM3D-NX v7.0.0 through v7.4.0. DREAM.3D v6.5.171 was not affected. | Counts real features and reserves exactly one background tuple. | +| `CAxisSegmentFeaturesFilter-D7` | Matching but undersized cell arrays could pass preflight and be read outside their bounds. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | Checks input and CellData tuple counts against the geometry (`-652`, `-653`). | +| `CAxisSegmentFeaturesFilter-D8` | Quats outside geometry CellData made execution look for FeatureIds at the wrong path. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | Preflight and execution both derive FeatureIds from geometry CellData. | +| `CAxisSegmentFeaturesFilter-D9` | Cancellation could report no features instead of returning cleanly. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | Checks cancellation before the no-feature error. | ## Code path coverage -*20 of 20 paths enumerated; 18 exercised.* - -Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/CAxisSegmentFeatures.cpp` (207 lines) + shared driver `src/simplnx/Utilities/SegmentFeatures.cpp` (execute loop). - -Logical phases: **(a) init + validation** in `operator()`, **(b) flood-fill driver** in `SegmentFeatures::execute`, **(c) seeding** in `getSeed`, **(d) grouping decision** in `determineGrouping`, **(e) finalize** in `operator()`. - -| # | Phase | Path | Test case | -|----|-------|--------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------| -| 1 | (a) | Mask array missing/wrong type at execute → error `-8362` | *Not directly tested. Defensive guard; `ArraySelectionParameter` validates the path through the normal IFilter API.* | -| 2 | (a) | Validation skips phase ≤ 0 cells | `Phase 0 (Unindexed) Cells Tolerated` | -| 3 | (a) | Validation skips masked-out cells | `Masked Non-Hexagonal Cells Tolerated` | -| 4 | (a) | Phase value ≥ CrystalStructures tuples → error `-8364` | `Execute Error - Phase Out of Ensemble Bounds (-8364)` | -| 5 | (a) | Participating non-hex cell → error `-8363` | `Execute Error - Non-Hexagonal Crystal Structure (-8363)` | -| 6 | (a) | Hexagonal_High and Hexagonal_Low both accepted | All Class 1 tests (High); `Class 1 Analytical (Phase Separation)` (Low, ensemble 2) | -| 7 | (a) | Geometry fetched as `IGridGeometry` (Image and RectGrid) | All tests (Image); `Class 1 Analytical (RectGrid Geometry)` (RectGrid) | -| 8 | (b) | First seed obtained via `getSeed` (D1 pin) | `Class 1 Analytical (Mask Excludes Voxel 0)`; `Execute Error - No Features Found (-87000)` | -| 9 | (b) | Face (6-neighbor) scheme, incl. y-/z-stride branches and the x-fastest linearization | `Class 1 Analytical (Pure-Phi Chain, Face)`; `Class 1 Analytical (3D Linearization, 3x2x2)` (axis-asymmetric Phi field kills any dims-permutation mutant) | -| 10 | (b) | All-connected (26-neighbor) scheme | `Class 1 Analytical (Neighbor Scheme Face vs All)` — "All Connected Neighbors" section | -| 11 | (b) | Cancel requested → early return | *Not directly tested. Requires cancel-signal injection; low-value gap.* | -| 12 | (c) | Seed scan skips owned / masked / phase ≤ 0 cells | `Mask Excludes Voxel 0`; `Phase 0 (Unindexed) Cells Tolerated` | -| 13 | (c) | Seed found → stamp FeatureId (feature AM resized once, post-run, in phase (e)) | Every successful Class 1 test (`CheckActiveArray` tuple counts) | -| 14 | (c) | No seed remains → return −1, driver exits| Every test (loop termination); immediately in `No Features Found (-87000)` | -| 15 | (d) | Neighbor already owned or masked-out → reject | Chain fixtures (burst revisits owned neighbors); `Mask Excludes Voxel 0` (cells 0, 3) | -| 16 | (d) | Phases differ → reject | `Class 1 Analytical (Phase Separation)`; `Phase 0 (Unindexed) Cells Tolerated` | -| 17 | (d) | `w ≤ tol` → group | `Pure-Phi Chain` (Δ = 3°, 4°, 5° pairs)| -| 18 | (d) | `π − w ≤ tol` → group (antiparallel c-axes) | `Class 1 Analytical (Pi-Fold Antiparallel C-Axes)` | -| 19 | (e) | `foundFeatures < 1` → error `-87000`; else AM resize, Active refill, `Active[0]=0` | `No Features Found (-87000)` (error); `CheckActiveArray` in every passing test (success) | -| 20 | (e) | `RandomizeFeatureIds == true` → deterministic, non-identity shuffle preserving id 0 | `Class 4 Invariants (RandomizeFeatureIds)` (non-identity + determinism); `Class 4 Invariants (RandomizeFeatureIds Preserves Masked Zeros)` | - -Filter-level (preflight) paths — tolerance == 0 → `-655`, cell-array tuple mismatch → `-651`, cell arrays vs geometry cell count → `-652`, cell AttributeMatrix vs geometry cell count → `-653` — are covered by the four `Preflight Error` tests; SIMPL 6.4/6.5 argument conversion by `SIMPL Backwards Compatibility`. The cancel early-return added to `operator()` (clean return instead of `-87000` on cancel) shares path 11's not-directly-tested status. +31 of 44 paths exercised. Each uncovered row states the missing trigger. + +Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/CAxisSegmentFeatures.cpp` (421 lines), `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/CAxisSegmentFeaturesFilter.cpp` (256 lines), `src/simplnx/Utilities/SegmentFeatures.cpp` (1159 lines), and `src/simplnx/Utilities/ClusteringUtilities.cpp` (126 lines). + +Test names below omit their `OrientationAnalysis::CAxisSegmentFeaturesFilter: ` or `OrientationAnalysis::CAxisSegmentFeatures: ` prefix. The scan assigns temporary labels, merges equivalent labels and writes dense final IDs. + +| # | Phase | Path | Test case | +|---|-------|------|-----------| +| 1 | Validation | Mask cannot be instantiated → error `-8362` | *Not directly tested. Normal parameter validation rejects the invalid mask first.* | +| 2 | Validation | Phase zero → skip crystal-structure validation | `Phase 0 (Unindexed) Cells Tolerated` | +| 3 | Validation | Masked non-hexagonal cell → skip validation | `Masked Non-Hexagonal Cells Tolerated` | +| 4 | Validation | Participating phase exceeds ensemble bounds → error `-8364` | `Execute Error - Phase Out of Ensemble Bounds (-8364)` | +| 5 | Validation | Participating non-hexagonal phase → error `-8363` | `Execute Error - Non-Hexagonal Crystal Structure (-8363)` | +| 6 | Validation | Accept Hexagonal_High and Hexagonal_Low | `Class 1 Analytical (Phase Separation)` | +| 7 | Geometry | Accept Image and RectGrid through `IGridGeometry` | `Class 1 Analytical (Pure-Phi Chain, Face)`; `Class 1 Analytical (RectGrid Geometry)` | +| 8 | Labeling | Reject masked cell zero before assigning a label | `Class 1 Analytical (Mask Excludes Voxel 0)` | +| 9 | Labeling | Face connectivity uses X, Y and Z strides and clips image boundaries | `Class 1 Analytical (3D Linearization, 3x2x2)` | +| 10 | Labeling | All-connected mode joins edge and vertex neighbors | `Class 1 Analytical (Neighbor Scheme Face vs All)`; `FaceEdgeVertex Connectivity` | +| 11 | Cancellation | Stop during scans or before finalization without reporting `-87000` | *Not directly tested. No test injects cancellation.* | +| 12 | Labeling | Masked or phase-zero cell → retain label zero | `Class 1 Analytical (Mask Excludes Voxel 0)`; `Phase 0 (Unindexed) Cells Tolerated` | +| 13 | Labeling | No similar labeled neighbor → create a temporary label | `Class 1 Analytical (Pure-Phi Chain, Face)` | +| 14 | Finalization | No valid cell → error `-87000` | `Execute Error - No Features Found (-87000)`; `No Valid Voxels Returns Error` | +| 15 | Labeling | Neighbor has label zero → skip it | `Class 1 Analytical (Mask Excludes Voxel 0)` | +| 16 | Grouping | Different phases → do not join | `Class 1 Analytical (Phase Separation)` | +| 17 | Grouping | Angle is within tolerance → join | `Class 1 Analytical (Pure-Phi Chain, Face)` | +| 18 | Grouping | Antiparallel folded angle is within tolerance → join | `Class 1 Analytical (Pi-Fold Antiparallel C-Axes)` | +| 19 | Finalization | Resize feature matrix to N + 1; set Active[0] to zero and real features to one | `Class 1 Analytical (Pure-Phi Chain, Face)`; `Class 1 Analytical (RectGrid Geometry)` | +| 20 | Finalization | Randomization preserves partitions and background zero | `Class 4 Invariants (RandomizeFeatureIds)`; `Class 4 Invariants (RandomizeFeatureIds Preserves Masked Zeros)` | +| 21 | Preflight | Zero tolerance → error `-655` | `Preflight Error - Zero Tolerance (-655)` | +| 22 | Preflight | Selected cell arrays have different tuple counts → error `-651` | `Preflight Error - Cell array tuple count mismatch (-651)` | +| 23 | Preflight | Cell-array tuple count differs from geometry → error `-652` | `Preflight Error - Cell Arrays Smaller Than Geometry (-652)` | +| 24 | Preflight | Cell AttributeMatrix tuple count differs from geometry → error `-653` | `Preflight Error - Cell AttributeMatrix Smaller Than Geometry (-653)` | +| 25 | Output path | Quats outside CellData → create and access FeatureIds in geometry CellData | `Class 1 Analytical (Quats Outside Cell AttributeMatrix)` | +| 26 | Finalization | Randomization disabled → retain dense labels in first-occurrence order | `Class 1 Analytical (Pure-Phi Chain, Face)` | +| 27 | Input | Read Bool and UInt8 masks; no mask → treat every cell as unmasked | `Class 1 Analytical (Mask Excludes Voxel 0)` — both variants; `Class 1 Analytical (Pure-Phi Chain, Face)` | +| 28 | Validation | Repeated valid phase → reuse cached validation | `Class 1 Analytical (Pure-Phi Chain, Face)` | +| 29 | Input | Crystal-structure, phase, quaternion or mask read fails → propagate error | *Not directly tested. No fixture injects an input-store failure.* | +| 30 | Input | Fill two slice slots and replace the least recently used slot | `High Tolerance Merges All` — 15 slices and exact labels | +| 31 | Input | Requested slice already cached → update its use order | *Not directly tested. The nonperiodic forward scan loads each slice once.* | +| 32 | Grouping | Requested voxel lies outside both buffered slices → reject it | *Not directly tested. The driver supplies current or previous slice neighbors.* | +| 33 | Grouping | Disable slice buffers and use direct array callbacks | *Not directly tested. This filter enables slice buffers before its scan.* | +| 34 | Input | Unsupported mask storage → error `-8365` | *Not directly tested. The mask parameter permits only Bool and UInt8.* | +| 35 | Labeling | Adopt a neighbor label or unite different temporary labels | `FaceEdgeVertex Connectivity`; `Class 1 Analytical (3D Linearization, 3x2x2)` | +| 36 | Grouping | Both folded angle tests fail → keep separate features | `Class 1 Analytical (Pure-Phi Chain, Face)` | +| 37 | Dispatch | Use the in-memory equivalence table | `Class 1 Analytical (Pure-Phi Chain, Face)` — scenario harness | +| 38 | Finalization | Map positive labels to dense IDs; leave background zero | `Class 1 Analytical (Mask Excludes Voxel 0)` | +| 39 | Limits | Label capacity, allocation or temporary-store provider fails → return error | *Not directly tested. Fixtures do not reach capacity limits or inject provider failures.* | +| 40 | Output | FeatureIds read or write fails during labeling → propagate error | *Not directly tested. No fixture injects a FeatureIds-store failure.* | +| 41 | Finalization | Feature-matrix resize or randomized-label transfer fails → propagate error | *Not directly tested. No fixture injects these failures.* | +| 42 | Shared driver | Periodic face or complete-boundary merge | *Not directly tested. This filter does not enable the shared driver's periodic option.* | +| 43 | Validation | Negative phase → skip validation and labeling | *Not directly tested. The phase-exemption fixture uses zero.* | +| 44 | Dispatch | Use external equivalence storage | *Not directly tested. No fixture in this file requires an external store.* | ## Test inventory +All cases are in `src/Plugins/OrientationAnalysis/test/CAxisSegmentFeaturesTest.cpp`. There are 28 ordinary cases. All pass in CI. The hidden generator creates inputs and does not assert segmentation results. + | Test case | Status | Notes | |-----------|--------|-------| -| `Class 1 Analytical (Pure-Phi Chain, Face)` | new-for-V&V | 8-cell chain, 4 features; 8 element-wise FeatureId assertions + Active invariants. | -| `Class 1 Analytical (Pi-Fold Antiparallel C-Axes)` | new-for-V&V | Exercises the `(π − w) ≤ tol` branch (Φ=2° vs 176° → folded 6°). | -| `Class 1 Analytical (Neighbor Scheme Face vs All)` | new-for-V&V | DYNAMIC_SECTION over both schemes on a 2×2×1 diagonal fixture; Face → 4 features, All → 3. | -| `Class 1 Analytical (Mask Excludes Voxel 0)` | new-for-V&V | DYNAMIC_SECTION over bool + uint8 masks; **D1 regression pin** (pre-fix: phantom feature + shifted ids). | -| `Class 1 Analytical (Phase Separation)` | new-for-V&V | Identical orientations split at a phase boundary; also covers Hexagonal_Low acceptance. | -| `Class 1 Analytical (3D Linearization, 3x2x2)` | new-for-V&V | Pins the x-fastest linearization + y/z stride branches with an axis-asymmetric Phi field (added after adversarial review showed the original fixtures were invariant under dims permutation). | -| `Class 1 Analytical (Quats Outside Cell AttributeMatrix)` | new-for-V&V | Pins the preflight/execute FeatureIds path agreement when Quats lives in a sibling AttributeMatrix (pre-fix: execute dereferenced a null FeatureIds array). | -| `Class 1 Analytical (RectGrid Geometry)` | new-for-V&V | **D5 regression pin** (pre-fix: null-pointer crash on RectGrid input). | -| `Class 4 Invariants (RandomizeFeatureIds)` | new-for-V&V | Partition preservation, permutation of {1..4}, non-identity vs canonical labeling (kills a dead randomizer), same-seed determinism across two runs. | -| `Class 4 Invariants (RandomizeFeatureIds Preserves Masked Zeros)` | new-for-V&V | Masked cells keep FeatureId 0 through the shuffle (pins the 0→0 mapping guarantee). | -| `Phase 0 (Unindexed) Cells Tolerated` | new-for-V&V | **D4 regression pin** (pre-fix: spurious `-8363` on the 999 sentinel). | -| `Masked Non-Hexagonal Cells Tolerated` | new-for-V&V | **D4 regression pin** (masked cubic phase must not be validated). | -| `Execute Error - Non-Hexagonal Crystal Structure (-8363)` | new-for-V&V | Unmasked cubic cells rejected. | -| `Execute Error - Phase Out of Ensemble Bounds (-8364)` | new-for-V&V | **D4 regression pin** (pre-fix: out-of-bounds read). | -| `Execute Error - No Features Found (-87000)` | new-for-V&V | All cells masked; **D1 regression pin** (pre-fix: success with 1 phantom feature). | -| `Preflight Error - Zero Tolerance (-655)` | new-for-V&V | Preflight rejects tolerance == 0. | -| `Preflight Error - Cell Arrays Smaller Than Geometry (-652)` | new-for-V&V | Cell arrays consistent with each other but not with the geometry's cell count (pre-fix: out-of-bounds walk at execute). | -| `Preflight Error - Cell AttributeMatrix Smaller Than Geometry (-653)` | new-for-V&V | Cell arrays match the geometry but the FeatureIds-hosting AttributeMatrix does not. | -| `Preflight Error - Cell array tuple count mismatch (-651)` | kept | Synthetic tuple-count mismatch between Quats and Phases. | -| `SIMPL Backwards Compatibility` | kept | DYNAMIC_SECTION over SIMPL 6.4 + 6.5 conversion fixtures; UUID + argument-key conversion only. | -| *(retired)* `CAxisSegmentFeatures:Face` / `:All` / `:MaskFace` / `:MaskAll` | retired | Consumed the `segment_features_test_data.tar.gz` exemplar whose `CAxis_FeatureIds_*` arrays were generated from SIMPLNX output — a circular oracle (see provenance sidecar). Replaced by the Class 1 fixtures, which cover the same scheme × mask parameter cube with independent expected output. | - -All non-retired tests pass at the verified commit in **both** builds: in-core `NX-Com-Qt69-Vtk95-Rel` and OOC `simplnx-ooc-Rel` (20/20 each, 2026-07-24). The shared-driver fix (D1) was regression-checked against the full `EBSDSegmentFeatures` (8/8) and `ScalarSegmentFeatures` (5/5) suites in both builds, including per-sibling masked-voxel-0, all-cells-masked (`-87000`), and periodic-boundary-wrap regression pins. +| `OrientationAnalysis::CAxisSegmentFeatures: No Valid Voxels Returns Error` | kept | All-masked synthetic data must return `-87000`. | +| `OrientationAnalysis::CAxisSegmentFeatures: Randomize Feature IDs` | kept | Synthetic layered orientations give three real features; checks dense randomized IDs and four Active tuples. | +| `OrientationAnalysis::CAxisSegmentFeatures: High Tolerance Merges All` | kept | A 90° tolerance merges all 15×15×15 cells; asserts every FeatureId is 1 and Active has two tuples. | +| `OrientationAnalysis::CAxisSegmentFeatures: FaceEdgeVertex Connectivity` | kept | Checks five face-connected features and three all-connected features, including edge and vertex pair identities. | +| `OrientationAnalysis::CAxisSegmentFeatures: Generate Test Data` | kept | Hidden (`[.GenerateTestData]`). Writes synthetic 15×15×15 and 200×200×200 inputs; does not execute segmentation. | +| `OrientationAnalysis::CAxisSegmentFeatures:Face` | kept | Archive regression: compares every label with `CAxis_FeatureIds_Face`, expects 57 Active tuples and checks tuple shapes. | +| `OrientationAnalysis::CAxisSegmentFeatures:All` | kept | Archive regression: compares every label with `CAxis_FeatureIds_All`, expects 37 Active tuples and checks tuple shapes. | +| `OrientationAnalysis::CAxisSegmentFeatures:MaskFace` | kept | Archive regression: compares every label with `CAxis_FeatureIds_Mask_Face`, expects 31 Active tuples and checks tuple shapes. | +| `OrientationAnalysis::CAxisSegmentFeatures:MaskAll` | kept | Archive regression: compares every label with `CAxis_FeatureIds_Mask_All`, expects 25 Active tuples and checks tuple shapes. | +| `OrientationAnalysis::CAxisSegmentFeaturesFilter: Class 1 Analytical (Pure-Phi Chain, Face)` | new-for-V&V | Eight-cell chain; checks eight labels, four real features and Active values. | +| `OrientationAnalysis::CAxisSegmentFeaturesFilter: Class 1 Analytical (Pi-Fold Antiparallel C-Axes)` | new-for-V&V | Three cells; checks the antiparallel angle fold and two features. | +| `OrientationAnalysis::CAxisSegmentFeaturesFilter: Class 1 Analytical (Neighbor Scheme Face vs All)` | new-for-V&V | `Face Neighbors` and `All Connected Neighbors` variants check `[1,2,3,4]` and `[1,2,3,1]`. | +| `OrientationAnalysis::CAxisSegmentFeaturesFilter: Class 1 Analytical (Mask Excludes Voxel 0)` | new-for-V&V | `bool mask` and `uint8 mask` variants check `[0,1,1,0,2]` and three Active tuples (D1). | +| `OrientationAnalysis::CAxisSegmentFeaturesFilter: Class 1 Analytical (Phase Separation)` | new-for-V&V | Equal orientations in two phases give `[1,1,2,2]`; accepts Hexagonal_Low. | +| `OrientationAnalysis::CAxisSegmentFeaturesFilter: Class 1 Analytical (3D Linearization, 3x2x2)` | new-for-V&V | Checks all 12 labels and six features on a 3×2×2 orientation field with different values along each axis. | +| `OrientationAnalysis::CAxisSegmentFeaturesFilter: Class 1 Analytical (RectGrid Geometry)` | new-for-V&V | RectGrid with unequal X intervals gives `[1,1,2]` and three Active tuples (D5). | +| `OrientationAnalysis::CAxisSegmentFeaturesFilter: Class 1 Analytical (Quats Outside Cell AttributeMatrix)` | new-for-V&V | Quats and phases in a sibling AttributeMatrix still produce `[1,1,2]` in geometry CellData (D8). | +| `OrientationAnalysis::CAxisSegmentFeaturesFilter: Class 4 Invariants (RandomizeFeatureIds)` | new-for-V&V | Two runs check partition preservation, IDs 1 through 4, a nonidentity permutation and determinism. | +| `OrientationAnalysis::CAxisSegmentFeaturesFilter: Class 4 Invariants (RandomizeFeatureIds Preserves Masked Zeros)` | new-for-V&V | Checks that masked cells retain zero and that two real features remain distinct after randomization. | +| `OrientationAnalysis::CAxisSegmentFeaturesFilter: Phase 0 (Unindexed) Cells Tolerated` | new-for-V&V | Phase-zero background stays zero while indexed cells form one feature (D4). | +| `OrientationAnalysis::CAxisSegmentFeaturesFilter: Masked Non-Hexagonal Cells Tolerated` | new-for-V&V | A masked cubic cell stays zero; the two hexagonal cells remain separate (D4). | +| `OrientationAnalysis::CAxisSegmentFeaturesFilter: Execute Error - Non-Hexagonal Crystal Structure (-8363)` | new-for-V&V | Participating cubic cells return `-8363`. | +| `OrientationAnalysis::CAxisSegmentFeaturesFilter: Execute Error - Phase Out of Ensemble Bounds (-8364)` | new-for-V&V | Phase 7 with two ensemble entries returns `-8364` (D4). | +| `OrientationAnalysis::CAxisSegmentFeaturesFilter: Execute Error - No Features Found (-87000)` | new-for-V&V | All three cells masked; requires error `-87000` (D1 and D6). | +| `OrientationAnalysis::CAxisSegmentFeaturesFilter: Preflight Error - Zero Tolerance (-655)` | new-for-V&V | Zero tolerance returns preflight error `-655`. | +| `OrientationAnalysis::CAxisSegmentFeaturesFilter: Preflight Error - Cell Arrays Smaller Than Geometry (-652)` | new-for-V&V | Nine input tuples for ten cells return `-652` (D7). | +| `OrientationAnalysis::CAxisSegmentFeaturesFilter: Preflight Error - Cell AttributeMatrix Smaller Than Geometry (-653)` | new-for-V&V | Ten input tuples with a nine-tuple CellData matrix return `-653` (D7). | +| `OrientationAnalysis::CAxisSegmentFeaturesFilter: Preflight Error - Cell array tuple count mismatch (-651)` | kept | Quats and phases with different tuple counts return `-651`. | +| `OrientationAnalysis::CAxisSegmentFeaturesFilter: SIMPL Backwards Compatibility` | kept | `SIMPL 6.5 (UUID)` and `SIMPL 6.4 (Filter_Name)` variants check filter identity and converted arguments. | ## Exemplar archive -- **Archive:** None — fixtures inlined in `test/CAxisSegmentFeaturesTest.cpp` (namespace `AnalyticalFixtures`). -- **SHA512:** N/A -- **Provenance:** `src/Plugins/OrientationAnalysis/vv/provenance/CAxisSegmentFeaturesFilter.md` (documents the retired circular-oracle archive consumption). - -## Deviations from DREAM3D 6.5.171 +- **Archive:** `segment_features_test_data.tar.gz` +- **SHA512:** `317d69384330d40c673f8e1a42df003dede5ac85331b2549e9f45467f7af6b74284f8dad1120427690719ebcd5066830e17031533381cc2cc0cc8622a422b914` +- **Provenance:** `src/Plugins/OrientationAnalysis/vv/provenance/CAxisSegmentFeaturesFilter.md` -Comparison run 2026-07-22, extended with a 3-D fixture and fully rerun 2026-07-24, on five pure-Phi fixtures (chain, π-fold, bool mask, phase-0, and a 3×2×2 masked fixture covering the y/z stride branches) through the official 6.5.171 PipelineRunner and nxrunner from one shared legacy-format input (`vv/comparisons/CAxisSegmentFeaturesFilter/`). All five match at the segmentation-partition level with identical feature counts. +## Deviations from DREAM.3D v6.5.171 -- `CAxisSegmentFeaturesFilter-D1` — unvalidated first seed in DREAM3D-NX 7.4.1 (the only affected release) could add a phantom feature or grow from a masked voxel — **resolved** — see `vv/deviations/CAxisSegmentFeaturesFilter.md` -- `CAxisSegmentFeaturesFilter-D2` — 6.5.171 always clock-randomizes FeatureIds (irreproducible); SIMPLNX is deterministic with opt-in randomization — see `vv/deviations/CAxisSegmentFeaturesFilter.md` -- `CAxisSegmentFeaturesFilter-D3` — SIMPLNX rejects non-hexagonal participating cells (`-8363`/`-8364`); legacy silently produced meaningless output — see `vv/deviations/CAxisSegmentFeaturesFilter.md` -- `CAxisSegmentFeaturesFilter-D4` — pre-fix SIMPLNX spuriously rejected unindexed/masked cells — **resolved** — see `vv/deviations/CAxisSegmentFeaturesFilter.md` -- `CAxisSegmentFeaturesFilter-D5` — pre-fix SIMPLNX crashed on RectGrid input — **resolved** — see `vv/deviations/CAxisSegmentFeaturesFilter.md` -**Fixed in DREAM3D-NX 7.4.2:** `CAxisSegmentFeaturesFilter-D1`, `CAxisSegmentFeaturesFilter-D4`-`D5`. +See `vv/deviations/CAxisSegmentFeaturesFilter.md`. This table lists Part 1; fixed SIMPLNX defects are in Part 2. +| Deviation | Observed difference | +|-----------|---------------------| +| `CAxisSegmentFeaturesFilter-D2` | DREAM.3D v6.5.171 always randomizes FeatureIds with a clock seed; SIMPLNX defaults to ordered labels and offers deterministic randomization. | +| `CAxisSegmentFeaturesFilter-D3` | SIMPLNX rejects participating non-hexagonal phases and invalid ensemble indices; DREAM.3D v6.5.171 does not apply these checks. | diff --git a/src/Plugins/OrientationAnalysis/vv/ComputeAvgCAxesFilter.md b/src/Plugins/OrientationAnalysis/vv/ComputeAvgCAxesFilter.md index b988d8e253..88c8a4560d 100644 --- a/src/Plugins/OrientationAnalysis/vv/ComputeAvgCAxesFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/ComputeAvgCAxesFilter.md @@ -1,182 +1,122 @@ # V&V Report: ComputeAvgCAxesFilter -| | | -|-----------|--------------------------| -| Plugin | OrientationAnalysis | -| SIMPLNX UUID | `453cdb58-7bbb-4576-ad5e-f75a1c54d348` | -| SIMPLNX Human Name | Compute Average C-Axis Orientations | -| DREAM3D 6.5.171 equivalent | `FindAvgCAxes` (SIMPL UUID `c5a9a96c-7570-5279-b383-cc25ebae0046`) — `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/FindAvgCAxes.{h,cpp}` in DREAM3D 6.5.171 | -| Verified commit | ** | -| Status | COMPLETE | -| Sign-off | Michael Jackson <mike.jackson@bluequartz.net> — 2026-05-28 | +| | | +|--------|--------------| +| Plugin | OrientationAnalysis | +| SIMPLNX UUID | `453cdb58-7bbb-4576-ad5e-f75a1c54d348` | +| SIMPLNX Human Name | Compute Average C-Axis Orientations | +| DREAM.3D v6.5.171 equivalent | `FindAvgCAxes` (SIMPL UUID `c5a9a96c-7570-5279-b383-cc25ebae0046`) | +| Verified commit | ** | +| Status | COMPLETE | +| Sign-off | Michael Jackson, 2026-05-28; second engineer: Nathan Young, 2026-06-09 | ## At a glance -| Aspect | Current state | -|------------------------|--------------------------| -| Algorithm Relationship | **Port with Minor Changes** — float→double accumulation + counter-reorder + error-code re-numbering (PR #1438); EbsdLib 2.0 API (PR #1472); cancel checks (PR #1582); `counter==0`→NaN finalize + per-feature normalize (V&V refactor). 7 deltas total — see Algorithm Relationship. | -| Oracle (confirmed) | **Class 1 (Analytical)** — 11-cell hand-built dataset plus a 4,097-cell real-HDF5 block-tail fixture, with closed-form `AvgCAxes` values; 10 of 12 code paths covered. **Class 4 (Invariant)** — `||AvgCAxes||==1.0` for hex-valid; `NaN` for empty/non-hex. F7 only asserts unit-vector magnitude (direction is precision-sensitive at the antipodal-flip cancellation boundary). Class 3 N/A. | -| Code paths enumerated | 10 of 12 exercised; the two cancellation paths remain outside this regression. | -| Tests today | 5 registered cases: analytical oracle, all-non-hex error, tuple-mismatch preflight error, phase-index bounds, and SIMPL 6.4/6.5 compatibility. One hidden OOC-only case verifies a 4,096-tuple block plus one-cell tail on actual HDF5 stores. | -| Exemplar archive | **`7_2_AvgCAxis.tar.gz` retired** — confirmed legacy-by-reputation oracle: reference values produced by a "special build of DREAM3D 6.6.379 with micro-texture bug fixes," not by an independent oracle. Per policy line 33, not eligible as a correctness oracle. Replaced by `compute_avg_c_axis.tar.gz` (hand-built Class 1 dataset, this V&V cycle). | -| Legacy comparison | **Complete (Run — SIMPLNX vs DREAM3D 6.5.171, post-normalize).** Two deviation classes, 4 feature-level differences vs 6.5.171: D1 (`counter==0` → NaN vs `(0,0,1)` rescue) at F0/F5/F6; D2 (precision-sensitive direction at antipodal-flip cancellation boundary + unit-vector vs unnormalized magnitude) at F7. Each root cause was proven by applying the corresponding surgical fixes to a local build of the legacy source, after which the legacy output became bit-identical to SIMPLNX across all 8 features. See `vv/comparisons/ComputeAvgCAxesFilter/results/three_way_comparison.txt`. | -| Bug flags | None confirmed. PR #1438's silent semantic changes are deviation candidates, not bugs.| -| V&V phase | **Phases 1, 2, 3 (Port classification), 4, 5, 6, 7 (algorithm review + fix-up), 8 (test restructure), 9 (legacy comparison), 11 (filter doc review) — complete.** SIMPLNX bit-identically matches the Class 1 oracle on F0-F6 and the Class 4 unit-vector invariant on F7. Legacy A/B confirmed against DREAM3D 6.5.171 (`/Users/mjackson/Applications/DREAM3D.app/Contents/bin/PipelineRunner`); each root cause was proven by applying the corresponding surgical fixes to a local build of the legacy source, after which the legacy output became bit-identical to SIMPLNX across all 8 features. **Outstanding:** Phase 10 (re-tar archive), Phase 12 (archive bundle: `download_test_data` + `TestFileSentinel` defaults), Phase 13 (status promotion DRAFT → COMPLETE).| +| Aspect | Current state | +|--------|---------------| +| Algorithm Relationship | Minor changes to `FindAvgCAxes` in DREAM.3D v6.5.171. SIMPLNX adds phase selection, double-precision orientation calculations, NaN output for empty features, and final normalization. | +| Oracle (confirmed) | Classes 1 and 4 are encoded in `test/ComputeAvgCAxesTest.cpp`. The analytical fixture checks component values, NaN values, and unit-vector magnitudes. | +| Code paths enumerated | 15 of 29 exercised by ordinary test cases. Cancellation, I/O failures, Hexagonal_Low, additional input conditions, and the full-block boundary lack ordinary fixtures. | +| Tests today | 6 test cases: 5 ordinary cases cover the oracle, input errors, and SIMPL conversion; 1 hidden case checks a block boundary. | +| Exemplar archive | `compute_avg_c_axis.tar.gz` supplies hand-built inputs. It replaces `7_2_AvgCAxis.tar.gz`; expected values are encoded in C++. | +| Legacy comparison | Not run — comparison artifacts are not available. | +| Bug flags | None. | +| V&V phase | COMPLETE. | ## Summary -`ComputeAvgCAxesFilter` computes a per-feature average C-axis direction (unit vector in the sample reference frame) for **hexagonal**-phase grains, by rotating the crystallographic `[001]` of each cell into the sample frame, applying a running-average antipodal-flip rule to keep contributions in a coherent hemisphere, and normalizing the final per-feature sum. Verification used a **Class 1 (Analytical) hand-built 11-cell / 8-feature dataset** with closed-form expected values for F0–F6 plus a **Class 4 unit-vector invariant** (`||AvgCAxes|| == 1.0`) for F7, which was deliberately placed on the antipodal-flip cancellation boundary; results were cross-checked against DREAM3D 6.5.171 (official release). Each root cause was proven by applying the corresponding surgical fixes to a local build of the legacy source, after which **the legacy output became bit-identical to SIMPLNX across all 8 features**, conclusively isolating the four documented differences vs 6.5.171 (D1 — `counter==0 → NaN` vs `(0,0,1)` rescue at F0/F5/F6; D2 — precision-sensitive direction + unit-vector vs unnormalized magnitude at F7) to the SIMPLNX-era design changes (float→double accumulation, Eigen-style math, `counter==0 → NaN` at finalize, and final per-feature normalize). +`ComputeAvgCAxesFilter` computes a unit average C-axis for each feature from its hexagonal-phase cells. Class 1 component expectations and Class 4 unit-vector invariants check the output. The filter has three fixed defects and two documented differences from DREAM.3D v6.5.171. ## Algorithm Relationship +*Classification:* Minor changes -*Classification:* **Port** with Minor Changes - -*Evidence:* The SIMPLNX algorithm at `Algorithms/ComputeAvgCAxes.cpp` (206 lines) is a translation of the legacy `FindAvgCAxes::execute()` from `DREAM3D/Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/FindAvgCAxes.cpp` (DREAM3D 6.5.171). -- Same SIMPL UUID retained via `OrientationAnalysisLegacyUUIDMapping.hpp` + SIMPL 6.4 and 6.5 conversion fixtures at `test/simpl_conversion/6_*/ComputeAvgCAxesFilter.json`. -- The control flow is preserved: phase-validity preflight → per-cell accumulation loop (passive-quaternion → orientation matrix → transpose → c-axis · `[0,0,1]`, antipodal-flip aware) → per-feature finalize (divide-by-count or `counter==0` handling). -- However, PR #1438 ("ENH: Microtexture related filter cleanup") applied intentional silent changes that distinguish this from a pure line-by-line port; -- PR #1472 applied an EbsdLib API refactor; -- PR #1582 added cancel checks -- Phase-7 V&V refactor consolidated the no-valid-contributions handling AND added a final per-feature normalize step so the output contract is unit C-axis vectors. All seven changes are listed below. - -*Port-time deltas (numerical / semantic Deviations vs legacy — see Phase 9):* - -1. **Float → double accumulation** (PR #1438) — All inner-loop accumulation now uses `Eigen::Vector3d`, `OrientationD`, `QuatD`, `Matrix3dR`. Legacy used `float` throughout. Contributes to Deviation D2 (precision-boundary antipodal flip). -2. **`counter[currentFeatureId]++` reordered** (PR #1438) — Moved from *after* the antipodal-flip dot-product test to *before* it. Changes which divisor is used for the first voxel's running-average reference; can produce sign flips on the first voxel of each feature vs. legacy. Contributes to Deviation D2. -3. **Error codes re-numbered + preflight warning removed** (PR #1438) — Legacy `-6402` → SIMPLNX `-76402`; legacy `-6403` → `-76403`; legacy preflight warning `-6401` ("Selected crystal structure phase is not Hexagonal") was *deleted entirely* and replaced with a non-error `preflightUpdatedValues` "Crystal Symmetry Warning": entry. API-surface Deviation candidate (no numerical impact on this V&V dataset). -4. **EbsdLib API refactor** (PR #1472) — `EbsdLib::` → `ebsdlib::` namespace; explicit `OrientationTransformation::qu2om(...)` + `OrientationMatrixToGMatrixTranspose(oMatrix)` helper call replaced by inline `ebsdlib::QuaternionDType(...).toOrientationMatrix()` + `oMatrix.transpose() * cAxis`. Convention parity between the new `toOrientationMatrix()` and the legacy `qu2om` verified on the V&V dataset (the surgically patched local build of the legacy source, still using legacy EbsdLib, bit-matches SIMPLNX, so the conventions are functionally aligned). -5. **Cancel checks added** (PR #1582) — `m_ShouldCancel` guards in the per-cell loop and the per-feature normalization loop. UX-only; no algorithmic effect on completed runs. -6. **`counter==0` → NaN at feature loop, feature loop starts at 0** (Phase-7 V&V refactor) — In the per-cell loop, non-hex cells are simply `continue`d (no in-place NaN write, no counter increment). The per-feature finalize loop now starts at `currentFeatureId = 0` (legacy starts at 1) and writes `(NaN, NaN, NaN)` whenever `counter[feature] == 0`. This consolidates all "no valid contributing voxels" handling at finalize and signals undefined output honestly. Drives Deviation D1 (F0, F5, F6). -7. **Final per-feature normalize** (Phase-7 V&V refactor) — After dividing the accumulated sum by `cellCount`, the result is now normalized to a unit C-axis vector before being stored. Legacy 6.5.171 returned the unnormalized average — magnitude was implicitly a within-feature coherence signal (1.0 for aligned, lower for diverging). The unit-vector contract better matches user expectations and downstream filters (which compute `acos(dot(a,b))` and need unit-magnitude inputs anyway). The antipodal-flip invariant guarantees `|sum| >= sqrt(cellCount)` so the divisor is never near-zero — no NaN guard needed. Widens Deviation D2 to include a magnitude difference (1.0 vs 2/3) at F7 in addition to the precision-sensitive direction. - -*Material PRs since baseline (2025-10-01):* - -- **#1438** — "ENH: Microtexture related filter cleanup" (merge `e6896714b`, 2025-10-25) — algorithm `+89/-76`, filter `+2/-3`. Float→double, counter-reorder, error re-numbering / preflight-warning removal. *(Normally pruned as a broad refactor, but promoted here per the audit's exception rule because it materially altered this filter's inner loop.)* -- **#1472** — "ENH: Update to EbsdLib 2.0.0 API" (merge `413e6fa46`, 2025-11-24) — algorithm `+9/-12`. EbsdLib namespace + `toOrientationMatrix()` refactor. *(Promoted from broad-refactor exclusion list — this filter delegates orientation math to EbsdLib.)* -- **#1476** — "BUG/ENH: Fix Backward Pipeline Compatibility and Add Testing" (merge `e45bca2a5`, 2026-01-06) — filter `+1/-1`. Single-line `FromSIMPLJson` converter type correction (`DataArrayNameFilterParameterConverter` → `DataArrayCreationToDataObjectNameFilterParameterConverter`) for the `AvgCAxes` output. -- **#1582** — "ENH: Add missing cancel checks to lots of filters" (merge `1a42ec6fb`, 2026-04-08) — algorithm `+10/-0`. Two `m_ShouldCancel` guards. -- **#1588** — "ENH: SIMPL Backwards Compatibility Test Redesign" (merge `f854bb636`, 2026-04-22) — test `+49`, plus two new fixture files `simpl_conversion/6_4/ComputeAvgCAxesFilter.json` and `simpl_conversion/6_5/ComputeAvgCAxesFilter.json`. SIMPL conversion test only. -- *(excluded — doc typo)* #1547 — "DOC: Fix filter documentation and documentation related code bugs" (2026-03-10) — docs `+1/-1` ("Crystallographic" → "Crystallography" subgroup typo). No algorithm or API change. -- *(pruned — broad refactor, no behavioral change to this filter)* #1439 (NeighborList tuple API), #1457 (static-inline cleanup), #1501 (Vec3 unification), #1538 (zlib tar.gz extraction in tests). +*Evidence:* `OrientationAnalysisLegacyUUIDMapping.hpp` maps `FindAvgCAxes` to this filter; both sources rotate crystal `[001]`, align antipodal contributions, and average by feature, while SIMPLNX adds phase filtering and final normalization. ## Oracle -*Class:* **1 (Analytical)** primary, **4 (Invariant)** companion. - -### Phase 2 exemplar-provenance finding - -The pre-existing exemplar `7_2_AvgCAxis.tar.gz` is a regression-style oracle pinned to a custom DREAM3D 6.6.379 "micro-texture special build" (per its inline ReadMe). Per V&V policy line 33, legacy output is never a valid correctness oracle — doubly disqualified here, since the reference build isn't even 6.5.171 baseline. **Retired** in favor of the hand-built Class 1 dataset below. - -### Applied (Class 1 — Analytical) - -Expected `AvgCAxes` values are derived by hand from input definitions without reference to any DREAM3D implementation. For each hex-feature `f`: - -1. Identify the cells assigned to `f`: `cellSet = {i : FeatureIds[i] == f}`. -2. For each cell `i` in `cellSet`: compute the voxel c-axis `c_i = R(q_i).T · [0,0,1]` where `R(q)` is the standard quaternion → passive orientation matrix (standard 2015 Eq. 14 convention; the algorithm's `oMatrix.transpose() * (0,0,1)` is the active-rotation form). -3. Apply the running-average antipodal flip rule: process cells in cell-index order; for each cell after the first, if `c_i · (Σ prior c_j / count) < 0`, flip `c_i := -c_i`. -4. Sum the (possibly-flipped) `c_i`, divide by the cell count, then normalize: `AvgCAxes[f] = normalize((Σ c_i) / count)`. The antipodal-flip invariant guarantees `|Σ c_i| >= sqrt(count)`, so dividing by `|Σ c_i / count|` is always well-defined whenever `count > 0`. - -**The output is a unit C-axis direction per feature** — magnitude is exactly 1.0 (within float32 epsilon) for every feature with at least one hex-phase contributing voxel. - -Hand-derivation on the 11-cell hand-built fixture (`FeatureIds = [1,2,3,3,3,4,4,6,7,7,7]`, `CellPhases = [1,1,1,1,1,1,1,2,1,1,1]`, hand-picked quaternions detailed in `vv/comparisons/ComputeAvgCAxesFilter/README.md`). **Class 1 fully covers F0-F6**; F7 is handled by the Class 4 unit-vector invariant below because the F7 cells were deliberately designed to land on the *antipodal-flip cancellation boundary* — the algorithm's choice between two genuinely distinct c-axis directions at this boundary is precision-sensitive (see Deviation D2). - -| Feature | Expected `AvgCAxes` | Magnitude | Path | -|---------|----------------------------|-----------|------------------------------------------------------------------------------------| -| 0 | NaN | — | placeholder; no cells reference it, `counter==0` at finalize → NaN| -| 1 | (0, 0, 1) | 1.0 | single voxel, identity | -| 2 | (0, 0.866025, 0.5) | 1.0 | single voxel, +60° about X | -| 3 | (0, 0, 1) | 1.0 | 3 aligned voxels — trivial average | -| 4 | (0, 0, 1) | 1.0 | antipodal pair + antipodal-flip resolution | -| 5 | NaN | — | no cells reference feature 5, `counter==0` at finalize → NaN | -| 6 | NaN | — | only cell of F6 is non-hex (Cubic_High) → skipped in cell loop, `counter==0` → NaN | -| 7 | *Class 4: magnitude = 1.0* | 1.0 | precision-sensitive boundary case — see below | - -**Feature 7 boundary case detail.** F7 has three cells with c-axes `(0, 0, 1)`, `(0, +√3/2, 0.5)`, `(0, -√3/2, 0.5)`. The Y-components of cells 9 and 10 are exact antipodes. At cell 10 the running-average dot product `c1 · normalize(avg/counter)` evaluates to `-0.433 + 0.433` — exact mathematical cancellation. Whichever side of zero the floating-point result lands on determines whether the antipodal flip fires, and after the final normalize this produces two **genuinely different** c-axis directions (NOT hex c≡-c equivalents — they make a 60° angle in 3D): +*Class:* 1 (Analytical), with 4 (Invariant) companions. -- **No flip** (pure-double math-ideal path): pre-normalize `(0, 0, 2/3)` → post-normalize `(0, 0, 1)` -- **Flip fires** (SIMPLNX faithful-float32 Eigen path): pre-normalize `(0, 1/√3, 1/3)` → post-normalize `(0, √3/2, 0.5)` +*Applied:* The 11-cell fixture gives component values for F1–F4 and NaN values for F0, F5, and F6; F7 checks unit magnitude because its sign decision lies at a rounding boundary. -Both are valid "average c-axes" of the same cell set under different sign-assignment choices for cells with c-axes pointing in opposite hemispheres. Both have magnitude `1.0` after normalization. The deviation is captured in Deviation D2; the unit test asserts only the invariant magnitude `1.0` for F7 (the direction is implementation-dependent). +*Encoded:* `src/Plugins/OrientationAnalysis/test/ComputeAvgCAxesTest.cpp::OrientationAnalysis::ComputeAvgCAxesFilter: Class 1 Oracle (hand-built dataset)` — 1 fixture, 8 features, and a general magnitude invariant; the five ordinary test cases use the supplied in-core CI evidence. All pass in CI. -### Applied (Class 4 — Invariant) - -Derivable properties asserted inline in test code (Phase 8 work): - -- **For features with `counter[i] > 0`** (i.e., at least one hex-phase contributing voxel): `||AvgCAxes[i]|| == 1.0` (within float32 ε). The Phase-7 finalize-normalize step guarantees every hex-valid feature is a unit C-axis vector. -- **`counter[i] == 0` → `(NaN, NaN, NaN)`** at finalize. Covers all three "no valid contributing voxels" scenarios: placeholder feature 0 (never referenced by any cell), feature with declared tuples but no cells assigned (F5), and feature whose cells are all non-hex (F6). -- **For F7 specifically**: the unit-vector invariant `||AvgCAxes[7]|| == 1.0` still holds; the direction is implementation-dependent at the precision boundary (see Deviation D2). - -### Encoded - -- **Class 1 (Analytical)**: `test/ComputeAvgCAxesTest.cpp::"Class 1 Oracle (hand-built dataset)"` — exact-value comparisons for F0–F6 per the table above (NaN checks for F0/F5/F6; exact component-wise checks for F1–F4). Encoded as a `DYNAMIC_SECTION` per feature. -- **Class 4 (Invariant)**: same test, with the `magnitude == 1.0` assertion for F7 plus a general invariant `DYNAMIC_SECTION` asserting `||AvgCAxes[i]|| == 1.0` over all hex-valid features. -- **Real-HDF5 block-tail (Class 1 + 4)**: `test/ComputeAvgCAxesTest.cpp::"genuine HDF5 4096-block tail oracle"` — 4,097 cells place identity and +60° X contributions on opposite sides of the 4,096-tuple bulk-read boundary. The literal expected result is the normalized +30° c-axis `(0, 0.5, √3/2)`; feature 0 remains NaN. Input and output stores must report `HDF5-OOC`. -- *(retained)* `test/ComputeAvgCAxesTest.cpp::"Invalid Filter Execution"` — all-non-hex error path (`-76402`) by mutating `crystalStructs[1] = 1` (Cubic_High). -- *(retained)* `test/ComputeAvgCAxesTest.cpp::"SIMPL Backwards Compatibility"` — SIMPL 6.4 + 6.5 conversion paths via `DYNAMIC_SECTION`. - -### Second-engineer review - -**Pending** +*Second-engineer review:* Skipped — the provenance records an analytical derivation from quaternion-rotated C-axes for the hand-built fixture, dated 2026-06-05. ## Bugs found and fixed -None. The existing D1 and D2 entries describe documented output conventions and precision effects. +All defects in this table are fixed. The fixes will be in the DREAM3D-NX release after v7.4.2. DREAM3D-NX v7.4.1 contains the fix for `ComputeAvgCAxesFilter-D5`. DREAM3D-NX v7.4.2 contains the fix for `ComputeAvgCAxesFilter-D4`. -## Code path coverage +| Deviation | Defect | Affected released versions | Resolution | +|-----------|--------|----------------------------|------------| +| `ComputeAvgCAxesFilter-D3` | A negative or out-of-range phase on a participating cell could read outside Crystal Structures. | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was not affected. | Returns error `-76404` before the lookup; skips background cells before checking their phases. | +| `ComputeAvgCAxesFilter-D4` | SIMPL conversion selected the cell matrix for feature output and supplied matrix paths for the phase and crystal-structure arrays. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | Uses the legacy output path for the feature matrix and derives conventional phase-array paths. | +| `ComputeAvgCAxesFilter-D5` | SIMPL conversion expected a string for the output name, but the legacy value is a path object. | DREAM3D-NX v7.0.0 through v7.4.0. DREAM.3D v6.5.171 was not affected. | Extracts the array name with `DataArrayCreationToDataObjectNameFilterParameterConverter`. | -*10 of 12 code paths exercised by unit tests. The two remaining gaps are the cancel-signal paths 8 and 12, which require mid-execution cancellation injection.* - -Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeAvgCAxes.cpp` (206 lines). - -The algorithm has three logical phases: (a) phase-validity preflight scan, (b) per-cell accumulation loop, (c) per-feature finalize loop. Each phase contains decision branches enumerated below. +## Code path coverage -| # | Phase | Path| Test case | -|----|-----------------|-----------|------------------------------------------------------------------------------------| -| 1 | (a) Preflight | All phases non-Hexagonal → return `-76402` error | `No_Hex_Phase` (mutates `CrystalStructures[1] = 1` to make all ensemble phases non-hex) | -| 2 | (a) Preflight | All phases Hexagonal → no error, no warning | `genuine HDF5 4096-block tail oracle` — the ensemble contains only `Unknown` and `Hexagonal_High`; the test asserts that execution returns no warnings. | -| 3 | (a) Preflight | Mixed phases (some Hex, some non-Hex) → warning `-76403` pushed, computation proceeds | `Class 1 Oracle` (ensemble has Hex_High + Cubic_High; warning fires once and the test sees it in the per-section warning log) | -| 4 | (b) Per-cell | `featureId == 0` (background) → skip cell (`if(currentFeatureId > 0)` guard) | `genuine HDF5 4096-block tail oracle` — 4,095 background cells must not contribute; feature 0 remains `(NaN, NaN, NaN)`. | -| 5 | (b) Per-cell | `featureId > 0` + non-Hex crystal struct → `continue` (no in-place NaN write; counter NOT incremented; NaN handled later at finalize) | `Class 1 Oracle` — F6 (sole cell is Cubic_High) verifies via the F6 = NaN assertion | -| 6 | (b) Per-cell | `featureId > 0` + Hex crystal struct → normal accumulation (passive→active rotation → unit-vector → running-average + antipodal flip → add) | `Class 1 Oracle` — F1, F2, F3 exact-value checks verify the rotation + accumulation | -| 7 | (b) Per-cell | Antipodal-flip branch: `CosBetweenVectors(c1, curCAxis) < 0` → `c1 *= -1` before accumulating | `Class 1 Oracle` — F4 (antipodal-pair → (0,0,1)) exact-value check + F7 magnitude invariant (cancellation-boundary case) | -| 8 | (b) Per-cell | Cancellation: `m_ShouldCancel` checked at top of per-cell loop → early return `result` | *Not tested.* Requires injecting a cancel signal mid-execution; not exercised by any current test. Low-value coverage gap. | -| 9 | (c) Per-feature | Per-feature finalize loop starts at `currentFeatureId = 0` (placeholder feature included)| `Class 1 Oracle` — F0 NaN check verifies the loop visits index 0 (legacy 6.5.171 leaves it untouched) | -| 10 | (c) Per-feature | `cellCount[i] == 0` → write `(NaN, NaN, NaN)` to feature's slot. Covers placeholder F0, no-cells features (F5), all-non-hex features (F6). | `Class 1 Oracle` — F0/F5/F6 NaN checks each cover one of the three sub-scenarios | -| 11 | (c) Per-feature | `cellCount[i] > 0` → divide x/y/z by `cellCount[i]`, then normalize (Phase-7 refactor) | `Class 1 Oracle` — F1–F4, F7 magnitude-1.0 invariants verify divide + normalize | -| 12 | (c) Per-feature | Cancellation: `m_ShouldCancel` checked at top of per-feature loop → early return `result`| *Not tested.* Same rationale as path 8. Low-value coverage gap. | +15 of 29 paths exercised. The count uses ordinary test cases; the hidden boundary fixture appears separately in the inventory. + +Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeAvgCAxes.cpp` (206 lines) and `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeAvgCAxesFilter.cpp` (174 lines). + +The algorithm scans the ensemble, accumulates cell contributions, and finalizes feature values. Test names below omit the common `OrientationAnalysis::ComputeAvgCAxesFilter:` prefix. + +| # | Phase | Path | Test case | +|---|-------|------|-----------| +| 1 | Ensemble | No hexagonal phase → error `-76402` | `No_Hex_Phase` | +| 2 | Ensemble | All non-placeholder phases are hexagonal → proceed without a symmetry warning | `Phase Index Bounds` — invalid phase on background only; execution succeeds | +| 3 | Ensemble | Mixed crystal structures → warning `-76403`, then proceed | `Class 1 Oracle (hand-built dataset)` — checks output; does not assert the warning code | +| 4 | Cells | FeatureId is zero → skip | `Phase Index Bounds` — invalid background phase does not cause an error | +| 5 | Cells | Non-hexagonal cell → skip contribution | `Class 1 Oracle (hand-built dataset)` — F6 is NaN | +| 6 | Cells | Hexagonal_High cell → rotate and accumulate | `Class 1 Oracle (hand-built dataset)` — F1–F3 component checks | +| 7 | Cells | Negative cosine → flip the C-axis | `Class 1 Oracle (hand-built dataset)` — F4 antipodal pair | +| 8 | Cells | Cancellation before a chunk → return | *Not directly tested. No fixture injects cancellation.* | +| 9 | Finalize | Visit feature zero | `Class 1 Oracle (hand-built dataset)` — F0 is NaN | +| 10 | Finalize | No hexagonal contributions → three NaN components | `Class 1 Oracle (hand-built dataset)` — F0, F5, F6 | +| 11 | Finalize | Positive count → average and normalize | `Class 1 Oracle (hand-built dataset)` — F1–F4 components and F7 magnitude | +| 12 | Finalize | Cancellation during feature finalization → return | *Not directly tested. No fixture injects cancellation.* | +| 13 | Preflight | Cell tuple counts differ → error `-6400` | `Preflight Error - Cell array tuple count mismatch (-6400)` | +| 14 | Cells | Participating phase is negative → error `-76404` | `Phase Index Bounds` — phase -1 | +| 15 | Cells | Participating phase exceeds the ensemble bounds → error `-76404` | `Phase Index Bounds` — phase 2 with two ensemble tuples | +| 16 | Cells | FeatureId is negative → skip | *Not directly tested. Fixtures do not assign negative FeatureIds.* | +| 17 | Cells | Hexagonal_Low cell → rotate and accumulate | *Not directly tested. Fixtures use Hexagonal_High.* | +| 18 | Cells | Participating phase zero maps to a non-hexagonal structure → skip | *Not directly tested. No successful fixture assigns phase zero to a positive FeatureId.* | +| 19 | Ensemble I/O | Crystal Structures read fails → return its error | *Not directly tested. No fixture injects an ensemble read failure.* | +| 20 | Cell I/O | FeatureIds read fails → return its error with existing warnings | *Not directly tested. No fixture injects this read failure.* | +| 21 | Cell I/O | Cell Phases read fails → return its error with existing warnings | *Not directly tested. No fixture injects this read failure.* | +| 22 | Cell I/O | Quaternions read fails → return its error with existing warnings | *Not directly tested. No fixture injects this read failure.* | +| 23 | Output I/O | Average C-Axes write fails → return its error with existing warnings | *Not directly tested. No fixture injects an output write failure.* | +| 24 | Cell I/O | Process a full 4,096-tuple chunk and a partial tail | *Not directly tested. Ordinary fixtures are smaller than one full chunk; the hidden `genuine HDF5 4096-block tail oracle` defines this check.* | +| 25 | Conversion | Convert legacy array paths and output name | `SIMPL Backwards Compatibility` — `SIMPL 6.5 (UUID)` and `SIMPL 6.4 (Filter_Name)` | +| 26 | Conversion | FeatureIds key is absent → skip derived phase paths | *Not directly tested. Both conversion fixtures contain the key.* | +| 27 | Conversion | Container or matrix name cannot be read → skip derived phase paths | *Not directly tested. Both conversion fixtures have valid path objects.* | +| 28 | Cells | Cosine is not negative → keep the C-axis sign | `Class 1 Oracle (hand-built dataset)` — aligned F3 cells | +| 29 | Cells | One feature contains both hexagonal and non-hexagonal cells → retain only the hexagonal contributions | *Not directly tested. Each fixture feature belongs to one phase.* | ## Test inventory -| Test case | Status | Notes | -|--------------------------------------------------------------------------------------|-------------|-----------| -| `OrientationAnalysis::ComputeAvgCAxesFilter: Class 1 Oracle (hand-built dataset)` | new-for-V&V | Replaces the retired `7_2_AvgCAxis` exemplar test. Encodes Class 1 + Class 4 oracle: exact-value checks for F0–F6 plus magnitude == 1.0 invariant for F7 and a general unit-vector invariant across all hex-valid features. | -| `OrientationAnalysis::ComputeAvgCAxesFilter:No_Hex_Phase` | kept | Retained; switched from the legacy `caxis_data` archive to the new hand-built input by mutating `CrystalStructures[1] = 1` (Cubic_High) to trigger `-76402` (all-non-hex error path). | -| `OrientationAnalysis::ComputeAvgCAxesFilter: Preflight Error - Cell array tuple count mismatch (-6400)` | new-for-OOC recertification | Verifies invalid preflight and exact error `-6400` when Quats, FeatureIds, and CellPhases tuple counts differ. | -| `OrientationAnalysis::ComputeAvgCAxesFilter: SIMPL Backwards Compatibility` | kept | Unchanged. `DYNAMIC_SECTION` over SIMPL 6.4 and 6.5 conversion fixtures (`test/simpl_conversion/6_*/ComputeAvgCAxesFilter.json`); validates UUID, argument keys, and parameter conversion only. | -| `OrientationAnalysis::ComputeAvgCAxesFilter: Phase Index Bounds` | new-for-OOC recertification | Uses actual HDF5 stores in the OOC build. An invalid participating phase returns `-76404`; an invalid phase on a background cell is skipped. | -| `OrientationAnalysis::ComputeAvgCAxesFilter: genuine HDF5 4096-block tail oracle` | new-for-OOC recertification | Hidden OOC-only Class 1/4 case. It verifies actual HDF5 input/output stores and the literal +30° average from contributions at tuple indices 4,095 and 4,096. | +| Test case | Status | Notes | +|-----------|--------|-------| +| `OrientationAnalysis::ComputeAvgCAxesFilter: Class 1 Oracle (hand-built dataset)` | new-for-V&V | Replaces the old exemplar-output comparison. Dynamic sections F0–F6 check three components or NaNs per feature; F7 checks magnitude; the general invariant checks features 1, 2, 3, 4, and 7. Component and magnitude tolerance is `1e-5`. | +| `OrientationAnalysis::ComputeAvgCAxesFilter: genuine HDF5 4096-block tail oracle` | new-for-V&V | Hidden. Requires HDF5 input and output stores; checks feature-zero NaNs and `(0, 0.5, √3/2)` from contributions at indices 4,095 and 4,096. | +| `OrientationAnalysis::ComputeAvgCAxesFilter:No_Hex_Phase` | kept | Reuses the hand-built input; changes phase 1 to Cubic_High and requires exactly one error, `-76402`. | +| `OrientationAnalysis::ComputeAvgCAxesFilter: Preflight Error - Cell array tuple count mismatch (-6400)` | new-for-V&V | Uses 10 quaternion and FeatureId tuples with 9 phase tuples; requires exactly one preflight error, `-6400`. | +| `OrientationAnalysis::ComputeAvgCAxesFilter: SIMPL Backwards Compatibility` | kept | Dynamic sections `SIMPL 6.5 (UUID)` and `SIMPL 6.4 (Filter_Name)` check identity, comments, and six converted arguments. The fixtures use the same matrix name for input and output, so they do not isolate the feature-matrix part of D4. | +| `OrientationAnalysis::ComputeAvgCAxesFilter: Phase Index Bounds` | new-for-V&V | Four generated combinations place phase -1 or 2 on a participating or background cell. Participating cells require `-76404`; background cells permit successful execution (D3). | -All five registered cases pass in both DREAM3D-NX builds. The hidden HDF5 block-tail case passes in the OOC build with 38 assertions. - -OOC recertification, 2026-09-18: serial CTest passed 5/5 in `NX-Com-Qt69-Vtk96-Rel` and 5/5 in `NX-Com-Qt69-Vtk96-OoC-Rel`. The new hidden boundary case passed 38 assertions in the OOC binary and is included in the OOC-only `OrientationAnalysisOocStoreContracts` CTest entry. The original report status and sign-off above are historical and unchanged. +The five ordinary test cases use the in-core CI result. All pass in CI. ## Exemplar archive - **Archive:** `compute_avg_c_axis.tar.gz` - **SHA512:** `4ee957b4a5e78e1d75e3585016a33de40985c66a8e8d1036b252e5974eb2b3360f34dacdcb51cd1d1ae25bfef2bb638979912cbc4555ff521eb2f42a167155b0` -- **Provenance:** Captured in the archive's internal `README.md` (no separate provenance file). Authoritative source for the input + scripts + pipelines + 3-way comparison outputs; mirrors the canonical V&V working set under `src/Plugins/OrientationAnalysis/vv/comparisons/ComputeAvgCAxesFilter/`. - -## Deviations from DREAM3D 6.5.171 - -**Two documented deviation classes (four feature-level differences total), all fully isolated** to *precision + matrix-math style + counter==0 NaN at finalize* by a surgically patched local build of the legacy source. The patch applies the SIMPLNX-era design changes (float→double accumulation, Eigen-style math, counter==0 → NaN at feature loop) back into the legacy DREAM3D 6.5 codebase and produces output **bit-identical** to SIMPLNX on the V&V hand-built fixture — conclusively isolating these design changes as the sole sources of the SIMPLNX-vs.-6.5.171 differences. +- **Provenance:** `src/Plugins/OrientationAnalysis/vv/provenance/ComputeAvgCAxesFilter.md` -Comparison fixtures: -- `/Users/mjackson/Workspace9/DREAM3D_Data/TestFiles/compute_avg_c_axis/output_legacy/6_5_171_compute_avg_c_axis.dream3d` (official DREAM3D 6.5.171 release output) -- a second output file alongside it in `output_legacy/` (output of the surgically patched local build of the legacy source) -- Three-way comparison report: `vv/comparisons/ComputeAvgCAxesFilter/results/three_way_comparison.txt` +## Deviations from DREAM.3D v6.5.171 -- `ComputeAvgCAxesFilter-D1` — `counter==0` → NaN vs `(0, 0, 1)` rescue. Fires at **F0** (placeholder feature, no cells), **F5** (no cells reference this featureId), and **F6** (all cells of this feature are non-hex). SIMPLNX writes NaN to signal "no valid contributing voxels"; legacy 6.5.171 falls into the rescue branch and writes a confusing `(0, 0, 1)`. See `vv/deviations/ComputeAvgCAxesFilter.md`. -- `ComputeAvgCAxesFilter-D2` — Per-feature direction may flip to an antipodal-equivalent representative under hex `c ≡ -c` symmetry; magnitude preserved. Fires at **F7** (precision-sensitive antipodal-flip cancellation boundary). Root cause: double-precision and Eigen-style matrix math vs. float-only hand-rolled math. See `vv/deviations/ComputeAvgCAxesFilter.md`. +See `vv/deviations/ComputeAvgCAxesFilter.md` for the source evidence, affected users, and recommendations. Part 1 contains the following entries. -**Comparison library nuance:** legacy DREAM3D 6.5.171 (and the patched local build of its source) uses a **built-in EbsdLib/OrientationLib** inside the DREAM3D source tree (frozen at the 6.5.171 release point, with the targeted surgical patches applied on top). SIMPLNX uses an **independent, vcpkg-installed EbsdLib** that is actively updated. Both implement standard conventions but the underlying code differs. The patched legacy build reproduces SIMPLNX *functional behavior*, not *identical library code* — sufficient to isolate the design-choice drivers of the deviations. +| Deviation | Observed difference | +|-----------|---------------------| +| `ComputeAvgCAxesFilter-D1` | SIMPLNX skips non-hexagonal cells and writes NaN when no hexagonal cells contribute. DREAM.3D v6.5.171 has no phase inputs, leaves feature zero at zero, and uses `(0,0,1)` for an empty positive feature. | +| `ComputeAvgCAxesFilter-D2` | SIMPLNX normalizes the final average and uses double-precision orientation calculations with float32 sums. DREAM.3D v6.5.171 uses float calculations and leaves the final average unnormalized. | diff --git a/src/Plugins/OrientationAnalysis/vv/ComputeAvgOrientationsFilter.md b/src/Plugins/OrientationAnalysis/vv/ComputeAvgOrientationsFilter.md index b06a35e624..1a86e53c11 100644 --- a/src/Plugins/OrientationAnalysis/vv/ComputeAvgOrientationsFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/ComputeAvgOrientationsFilter.md @@ -1,153 +1,148 @@ # V&V Report: ComputeAvgOrientationsFilter -| | | -|-----------|--------------------------| -| Plugin | OrientationAnalysis | -| SIMPLNX UUID | `086ddb9a-928f-46ab-bad6-b1498270d71e` | -| SIMPLNX Human Name | Compute Feature Average Orientations | -| DREAM3D 6.5.171 equivalent | `FindAvgOrientations` (SIMPL UUID `bf7036d8-25bd-540e-b6de-3a5ab0e42c5f`) — `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/FindAvgOrientations.{h,cpp}` | -| Verified commit | ** | -| Status | COMPLETE | -| Sign-off | Michael Jackson — 2026-07-16 | +| | | +|--------|--------------| +| Plugin | OrientationAnalysis | +| SIMPLNX UUID | `086ddb9a-928f-46ab-bad6-b1498270d71e` | +| SIMPLNX Human Name | Compute Feature Average Orientations | +| DREAM.3D v6.5.171 equivalent | `FindAvgOrientations` (OrientationAnalysis plugin, UUID `bf7036d8-25bd-540e-b6de-3a5ab0e42c5f`) | +| Verified commit | ** | +| Status | COMPLETE | +| Sign-off | Michael Jackson, 2026-06-30; second engineer: Nathan Young, 2026-07-01 | ## At a glance -| Aspect | Current state | -|------------------------|--------------------------| -| Algorithm Relationship | **Minor changes** (Rodrigues method) **+ New** (vMF/Watson). The Rodrigues average is a faithful port of legacy `FindAvgOrientations::execute()` with 4 deliberate deltas (positive-orientation canonicalization, FeatureId==0 inclusion, divide-by-zero cleanup, EbsdLib library swap). The von Mises-Fisher and Watson methods are **new** (added PR #1577), have **no legacy equivalent**, are off by default, and delegate their EM math to EbsdLib `DirectionalStats`. | -| Oracle (confirmed) | **Confirmed.** Rodrigues → **Class 1 (analytical)** + **Class 4 (invariant)**; vMF/Watson → **Class 2 (EbsdLib reference, trusted, not re-tested)** + **Class 4 (invariant)**, scoped to the filter's value-add per the "don't re-test upstream" rule. Encoded as 2 oracle TEST_CASEs (+1 error test) in `test/ComputeAvgOrientationsTest.cpp`; all pass. SIMPLNX matches the oracle on every fixture. | -| Code paths enumerated | 10 of 11 paths exercised (see Code path coverage); the one gap is the Watson-only dispatch branch (low-value array-selection path). | -| Tests today | 9 test cases: Rodrigues analytical (Class 1+4), Rodrigues cubic-symmetry invariant (Class 4, #1660), Rodrigues voxel-ordering independence (Class 4), vMF/Watson EbsdLib-reference (Class 2+4), vMF/Watson phase-0 exclusion regression (#1659), crystal-structure/phase guard warnings (#1661), no-method-enabled error (preflight -54673 + runtime backstop -54670), cell-array tuple-mismatch error (-651, code asserted), and SIMPL 6.4/6.5 backwards-compat (DYNAMIC_SECTION). All inline hand-built data — no exemplar archive. Closes the GCOV vMF/Watson coverage gap. | -| Exemplar archive | **None — retired `7_ComputeAvgOrientation_v2.tar.gz`** (it was a circular oracle regenerated from post-fix SIMPLNX output in PR #1577). Oracle is now inline in the test source. `download_test_data()` entry to be removed in Phase 10. | -| Legacy comparison | **Run (2026-06-30) vs the official DREAM3D 6.5.171 release.** Two fixtures. On the realistic 480k-cell / 408-feature input the two are **numerically equivalent on all real features** (`AvgQuats` ≤1e-6, zero sign flips; `AvgEulerAngles` max 4.77e-7). Divergences are confined to **feature-0 / unindexed-voxel handling**: **D3** (empty feature 0) and **D2** (FeatureId-0 voxels with phase>0 — demonstrated on a forcing fixture: NX computes the average, legacy writes `(0,0,0,0)`). **D4** sub-epsilon. **D1 downgraded** (no demonstrable divergence). vMF/Watson have no legacy equivalent (N/A). | -| Bug flags | One **legacy** bug, empirically confirmed: `ComputeAvgOrientationsFilter-D3` (6.5.171 leaves the zero-voxel feature 0 as `(0,0,0,0)` / divides zero-count features by zero; SIMPLNX writes a clean identity). D2 is a deliberate algorithmic-choice divergence (not a bug). One **SIMPLNX** bug found by the post-merge adversarial review and fixed 2026-07-08: **issue #1659** — the vMF/Watson gather loop ignored `Phases`, so a phase-0 voxel inside a feature contributed a garbage quaternion to the EM average. Fixed by gating the gather identically to the counting pass; pinned by the `vMF/Watson Ignores Phase-0 Voxels` regression test. | -| V&V phase | **Phases 1, 3, 4, 5, 6, 7, 8, 9 complete** — oracle confirmed/reconciled, review fixes applied, all 5 tests pass (in-core `NX-Com-Qt69-Vtk95-Rel`, vcpkg EbsdLib 3.0.0), legacy comparison run (numerically equivalent). OOC build skipped (per maintainer). V&V complete and signed off 2026-07-16 (Michael Jackson, technical authority). Outstanding: 11 (doc deviation links), 12 (OneDrive archive), 13 (tracking). | +| Aspect | Current state | +|--------|---------------| +| Algorithm Relationship | Minor changes to the Rodrigues method of `FindAvgOrientations` in DREAM.3D v6.5.171. SIMPLNX also provides von Mises-Fisher (vMF) and Watson methods, which have no legacy equivalent. | +| Oracle (confirmed) | Classes 1, 2 and 4: analytical Rodrigues values, EbsdLib directional estimates and invariants in `test/ComputeAvgOrientationsTest.cpp`. All pass in CI. | +| Code paths enumerated | 27 of 42 paths exercised. Gaps include two method combinations, invalid FeatureIds, external-sort contracts and defensive error paths. | +| Tests today | 15 test cases cover analytical values, reference estimates, method selection, phase handling, transfer failures, cancellation and SIMPL conversion. | +| Exemplar archive | None in use. `7_ComputeAvgOrientation_v2.tar.gz` was retired; the tests construct inputs and expected values in C++. | +| Legacy comparison | Not run on the current algorithm; the available DREAM.3D v6.5.171 comparison is dated 2026-09-17. Algorithm source changed after that run. | +| Bug flags | None. | +| V&V phase | COMPLETE. | ## Summary -`ComputeAvgOrientationsFilter` ("Compute Feature Average Orientations") computes a per-Feature average crystallographic orientation from per-Element (voxel) quaternions, using one or more of three independently-toggled methods: the original **Rodrigues** running-quaternion average (a port of legacy `FindAvgOrientations`), and two **new** Expectation-Maximization distribution fits — **von Mises-Fisher** and **Watson** — that delegate their sampling math to EbsdLib `DirectionalStats`. Verification establishes correctness independently of legacy: Rodrigues via a Class 1 analytical oracle (hand-built single- and multi-voxel features with closed-form averages) plus Class 4 invariants; vMF/Watson via Class 2 (EbsdLib as trusted reference, not re-tested) plus Class 4 invariants scoped to the filter's value-add. Headline result: SIMPLNX matches the oracle on every fixture and is numerically equivalent to the official DREAM3D 6.5.171 on all real features; the only legacy divergences are the deliberate feature-0 / unindexed-voxel handling (D2, D3) and a sub-epsilon precision difference (D4). All 5 unit tests pass. +`ComputeAvgOrientationsFilter` calculates average feature orientations with Rodrigues, vMF or Watson methods. Analytical fixtures, EbsdLib reference values and invariants check the results. All pass in CI. ## Algorithm Relationship -*Classification:* **Minor changes** (Rodrigues method, the legacy-equivalent output path) **+ New filter capability** (vMF/Watson methods, no legacy equivalent). +*Classification:* Minor changes -*Evidence:* Same SIMPL UUID retained (`bf7036d8-…` → SIMPLNX `086ddb9a-…`); SIMPL 6.4/6.5 conversion fixtures at `test/simpl_conversion/{6_4,6_5}/ComputeAvgOrientationsFilter.json` convert only the six legacy parameters (FeatureIds, Phases, Quats, CrystalStructures, AvgQuats, AvgEulerAngles) — i.e. the Rodrigues method. The legacy `FindAvgOrientations::execute()` (DREAM3D 6.5.171, 347 lines) computes **only** the Rodrigues average; `computeRodriguesAverage()` (`ComputeAvgOrientations.cpp` lines 367–457) preserves its control flow line-for-line (per-voxel running-average accumulation with `getNearestQuat`, then per-feature divide+normalize). The vMF/Watson methods were added in **PR #1577** (`parametersVersion` bumped 1→2) and are off by default. - -### Rodrigues port-time deltas (each a candidate Deviation — see `vv/deviations/ComputeAvgOrientationsFilter.md`) - -1. **Positive-orientation canonicalization** (D1 — **downgraded, not a deviation**). SIMPLNX appends `.getPositiveOrientation()` after normalize (line 448), forcing the average quaternion into the northern hemisphere; legacy `FindAvgOrientations` calls only `QuaternionMathF::UnitQuaternion` (normalize, line 270). Empirically (2026-06-30) this produces **no divergence** from 6.5.171 — symmetry reduction keeps the average at `w ≥ 0`, and the pure-triclinic edge case is the same physical orientation (`q ≡ −q`). Defensive normalization only. See deviations file. -2. **FeatureId == 0 inclusion** (candidate D2). Legacy gates accumulation on `m_FeatureIds[i] > 0 && m_CellPhases[i] > 0` (line 246). SIMPLNX gates on `currentPhase > 0` only (line 412) — deliberately allowing voxels with FeatureId 0 to be averaged, per the documented use-case of averaging an unlabeled bag of orientations (algorithm comment, lines 401–411). Root cause: algorithmic choice. **Observable only** when a dataset has FeatureId 0 with phase > 0 (atypical). -3. **Zero-count finalize cleanup** (candidate D3 — legacy bug). Legacy finalize loop (lines 263–274) starts at feature 1 and, for `counts[i] == 0`, sets identity but then still executes `ScalarDivide(avgQuats[i], counts[i])` — a divide by zero. SIMPLNX (lines 440–444) sets identity and `continue`s, skipping the divide. Root cause: bug in 6.5.171. **Observable** for any zero-voxel feature; legacy also never finalizes feature 0 (loop starts at 1), leaving it `(0,0,0,0)`. -4. **Library swap** (candidate D4 — library + precision). Legacy uses `QuaternionMathF` + `OrientationTransforms::qu2eu`; SIMPLNX uses EbsdLib `ebsdlib::QuatF` + `QuaternionFType::toEuler()`. Same `float32` precision. **Possible** sub-epsilon Euler differences from differing intermediate math/quaternion→Euler conventions. - -*Material PRs since the filter's creation:* **#1577** (added vMF/Watson + version 2 + regenerated the `_v2` exemplar — the circular-oracle source; commit note also says "Fix phaseIndex calculation bug"), **#1588** (SIMPL backwards-compat test redesign), **#1535** (removed redundant preflight checks now done by parameters), **#1476** (backwards pipeline compatibility fix), **#1472** (EbsdLib 2.0.0 API — cross-cutting EbsdLib-drift hotspot), **#1458** (EbsdLib 1.0.40), **#1438** (microtexture-related cleanup — cross-cutting hotspot). - -### Algorithm review (V&V cycle quality pass) - -Line-by-line review performed via the `review-algorithm` skill on the already-oracle-verified code. Findings applied (verified output-neutral — all tests still pass after rebuild): - -- **Thread safety:** `computeVmfWatsonAverage` now calls `dataAlg.setParallelizationEnabled(false)`. The worker read/wrote shared `DataArray`/`DataStore` objects concurrently, which the simplnx thread-safety policy forbids even at distinct indices. Serial execution matches the `ComputeFeatureFaceMisorientation` precedent. -- **Cancel checking:** added `m_Filter->getCancel()` (new accessor) to the vMF/Watson per-feature loop, which previously could not be cancelled during EM. -- **Progress messaging:** `computeRodriguesAverage` now emits throttled progress feedback over the per-cell loop. -- **Robustness:** guarded `ops[laueClass]` / `orientationOps[xtal]` against out-of-range `CrystalStructures` values (e.g. `999`/Unknown) to avoid undefined behavior; the feature/voxel is skipped (vMF/Watson outputs stay NaN). -- **Cosmetic:** removed commented-out dead code, fixed the `dataStruture` parameter typo, removed the unused `m_LastProgressInt` member. - -**Deferred (documented rationale):** the vMF/Watson voxel gathering is O(numFeatures × numVoxels) — each feature rescans all voxels rather than bucketing once. This is a *performance* optimization, not correctness; with parallelization now disabled it runs serially but remains acceptable for typical feature counts. Left as a follow-up to avoid restructuring verified code in this V&V pass. +*Evidence:* The legacy `FindAvgOrientations` and current Rodrigues path use nearest-symmetry quaternion accumulation; SIMPLNX adds feature-zero averaging, empty-feature cleanup and the optional vMF and Watson methods. ## Oracle -*Status: confirmed. Boundary with EbsdLib confirmed; second-engineer oracle review signed off by Michael Jackson (technical authority) 2026-07-16 (see provenance sidecar).* +*Class:* 1 (Analytical), 2 (Reference — EbsdLib) and 4 (Invariant). -*Class:* **1 (Analytical) + 4 (Invariant)** for Rodrigues; **2 (Reference — EbsdLib, trusted & not re-tested) + 4 (Invariant)** for vMF/Watson. +*Applied:* Hand-derived quaternion means check Rodrigues; fixed EbsdLib estimates check vMF and Watson; unit norm, symmetry, phase exclusion and empty-feature rules check invariants. -### The EbsdLib boundary (what we do NOT re-test) +*Encoded:* `src/Plugins/OrientationAnalysis/test/ComputeAvgOrientationsTest.cpp::OrientationAnalysis::ComputeAvgOrientations: Rodrigues Analytical Oracle` has six feature fixtures; `vMF/Watson EbsdLib Reference Oracle` has one 22-quaternion reference feature and a background tuple. All pass in CI. -The vMF/Watson EM math lives in EbsdLib `DirectionalStats::EMforDS` and is already covered by `EbsdLib/Source/Test/DirectionalStatsTest.cpp`. Critically, `DirectionalStatsTest:VMF` and `:Watson` run **at this filter's exact configuration** — `numEM=5, numIter=10, seed=43514` — on a fixed set of 22 hand-coded quaternions, and hard-assert the resulting `muhat` (±1e-6) and `kappahat`: -- VMF: `muhat = (w 0.88937, x 0.33220, y −0.19646, z 0.24507)`, `kappahat = 88.9943` -- WAT: `muhat = (w 0.90119, x 0.29483, y −0.21060, z 0.23787)`, `kappahat = 30.5730` +*Second-engineer review:* Michael Jackson, 2026-07-16 (oracle review recorded in the provenance sidecar). -EbsdLib owns: EM convergence/multi-restart, symmetry-aware E/M steps, kappa estimation, FZ mapping of the result, seeded-PRNG reproducibility. We trust those (Class 2 reference). We do **not** reproduce the EM algorithm. +## Bugs found and fixed -### Applied +All defects in this table are fixed. The fixes will be in the DREAM3D-NX release after v7.4.1. DREAM3D-NX v7.4.2 contains the fix for `ComputeAvgOrientationsFilter-D3`, `ComputeAvgOrientationsFilter-D5`, `ComputeAvgOrientationsFilter-D6`, `ComputeAvgOrientationsFilter-D7`, `ComputeAvgOrientationsFilter-D8` and `ComputeAvgOrientationsFilter-D9`. -- **Rodrigues (Class 1 — analytical):** Hand-built features with closed-form averages — a single-voxel feature averages to that voxel's FZ-reduced, northern-hemisphere quaternion; N identical-orientation voxels average to that same orientation; a symmetric pair about a known axis averages to the bisector. Expected quats/Eulers derived by hand. -- **Rodrigues (Class 4 — invariant):** unit-norm + northern-hemisphere (`w ≥ 0`) output quats; zero-voxel feature → identity `(0,0,0,1)` + zero Euler; result invariant to voxel ordering within a feature. -- **vMF/Watson (Class 2 — EbsdLib reference, reused not reinvented):** Construct a single SIMPLNX feature whose voxels are exactly EbsdLib's 22 reference quaternions (same single Laue class), run the filter, and assert the filter's per-feature `muhat`/`kappa` match EbsdLib's **already-asserted** VMF/WAT values above. This proves the filter feeds EbsdLib correctly and lands the result in the right tuple — without re-deriving any EM math. (If the 22 quats are not pre-FZ-reduced, account for the filter's `getFZQuat` step in the input or expected value.) -- **vMF/Watson (Class 4 — the filter's value-add):** single-voxel feature → `muhat` equals that voxel's FZ quat and `kappa == 0` (filter shortcut, algorithm lines 103–106 / 139–141, EM skipped); zero-voxel feature → all outputs NaN (fill at lines 339–355); correct per-feature voxel gathering + FZ reduction + phase→crystal-structure lookup; results written at the feature index (not voxel index); unit + northern-hemisphere quats. - -*Encoded:* -- `test/ComputeAvgOrientationsTest.cpp::"OrientationAnalysis::ComputeAvgOrientations: Rodrigues Analytical Oracle"` — Class 1 (6 Rodrigues fixtures, exact quats at 1e-6) + Class 4 (unit/northern, zero→identity, vMF/Watson single→FZ-quat & kappa 0, zero→NaN). -- `test/ComputeAvgOrientationsTest.cpp::"OrientationAnalysis::ComputeAvgOrientations: vMF/Watson EbsdLib Reference Oracle"` — Class 2 (22-quat feature reproduces EbsdLib's documented VMF/WAT muhat+kappa; quat margin 5e-3, kappa 2% to absorb the float32 input round-trip) + Class 4 (zero-voxel→NaN). -- `test/ComputeAvgOrientationsTest.cpp::"OrientationAnalysis::ComputeAvgOrientations: No Method Enabled Error"` — error path `-54670`. - -- `test/ComputeAvgOrientationsTest.cpp::"OrientationAnalysis::ComputeAvgOrientations: Rodrigues Cubic Symmetry Invariant"` — Class 4 under non-trivial (Cubic_High) symmetry: five representations of the same orientation (Rz(30°+90°k) and −Rz(30°)) must average to Rz(30°) (issue #1660). -- `test/ComputeAvgOrientationsTest.cpp::"OrientationAnalysis::ComputeAvgOrientations: Rodrigues Voxel Ordering Independence"` — Class 4: F3 fixture in both voxel orderings → identical Rz(45°). -- `test/ComputeAvgOrientationsTest.cpp::"OrientationAnalysis::ComputeAvgOrientations: vMF/Watson Ignores Phase-0 Voxels"` — regression pin for issue #1659. -- `test/ComputeAvgOrientationsTest.cpp::"OrientationAnalysis::ComputeAvgOrientations: Unknown Crystal Structure and Out-Of-Range Phase Guards"` — guard/warning coverage for issue #1661 (-54671/-54672). - -All 9 test cases pass in the `NX-Com-Qt69-Vtk95-Rel` build. OOC dual-build was skipped per maintainer (this filter's algorithm is feature-indexed with serial execution; no OOC-specific code path). - -*Second-engineer review:* **Signed off by Michael Jackson (technical authority), 2026-07-16.** +| Deviation | Defect | Affected released versions | Resolution | +|-----------|--------|----------------------------|------------| +| `ComputeAvgOrientationsFilter-D3` | Empty features produced an invalid quaternion or divided by zero. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | The filter finalizes tuple zero, writes identity and skips division for every empty feature. | +| `ComputeAvgOrientationsFilter-D5` | vMF/Watson gathering included phase-zero voxels that the counting pass excluded. | None. DREAM3D-NX v7.0.0 through v7.4.2 were not affected. DREAM.3D v6.5.171 was not affected. | The filter applies the same phase gate to counting and gathering. | +| `ComputeAvgOrientationsFilter-D6` | An invalid crystal-structure value indexed outside the orientation-operator vector. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | The filter checks the operator index, skips unsupported input and returns warning `-54671`. | +| `ComputeAvgOrientationsFilter-D7` | An invalid positive phase indexed outside Crystal Structures. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | The filter checks the phase range, excludes the voxel and returns warning `-54672`. | +| `ComputeAvgOrientationsFilter-D8` | vMF/Watson workers accessed shared array stores concurrently. | None. DREAM3D-NX v7.0.0 through v7.4.2 were not affected. DREAM.3D v6.5.171 was not affected. | The filter runs the resident worker serially. | +| `ComputeAvgOrientationsFilter-D9` | The vMF/Watson feature loop did not check cancellation. | None. DREAM3D-NX v7.0.0 through v7.4.2 were not affected. DREAM.3D v6.5.171 was not affected. | The filter checks cancellation before processing each feature. | ## Code path coverage -*15 of 16 paths exercised. Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeAvgOrientations.cpp`.* Logical phases: **(a)** `operator()` dispatch + output-array selection/validation, **(b)** Rodrigues two-pass average, **(c)** vMF/Watson per-feature EM (serial over features). Paths 12–16 were added by the #1659/#1661 follow-up work (2026-07-08). - -| # | Phase | Path | Test case | -|----|------------------|----|----------------------| -| 1 | (a) Dispatch | `avgEulerAnglesArrayPath` exists → use it to set `m_NumberOfFeatures` (Rodrigues enabled) | `Rodrigues Analytical Oracle` (Rodrigues on) | -| 2 | (a) Dispatch | else `VMFEulerAnglesArrayPath` exists → use it (vMF enabled, Rodrigues off) | `vMF/Watson EbsdLib Reference Oracle` (Rodrigues off, vMF on) | -| 3 | (a) Dispatch | else `WatsonEulerAnglesArrayPath` exists → use it (Watson enabled, Rodrigues & vMF off) | *Not directly tested. Low-value array-selection branch; only sets `m_NumberOfFeatures` source, equivalent to paths 1/2.* | -| 4 | (a) Dispatch | none exist → error `-54670` "A valid Feature level array … was not found" | `No Method Enabled Error` | -| 5 | (b) Rodrigues | `currentPhase > 0` → accumulate running nearest-quat average (incl. FeatureId 0) | `Rodrigues Analytical Oracle` — F1–F4 | -| 6 | (b) Rodrigues | `counts[fid] == 1` → reset running average to identity before getNearestQuat | `Rodrigues Analytical Oracle` — F1, F2 (single-voxel) | -| 7 | (b) Rodrigues | finalize `counts[fid] == 0` → write identity quat, `continue` (skip divide) | `Rodrigues Analytical Oracle` — F0, F5 (zero-voxel) | -| 8 | (b) Rodrigues | finalize `counts[fid] > 0` → divide, normalize, positiveOrientation, qu2eu | `Rodrigues Analytical Oracle` — F2, F3, F4 | -| 9 | (c) vMF/Watson | `featureNumVoxels[fid] == 0` → skip (output stays NaN from fill) | both oracle tests — F0/F5 and background feature → NaN | -| 10 | (c) vMF/Watson | `fzQuats.size() == 1` → muhat = single FZ quat, kappa = 0 (EM skipped) | `Rodrigues Analytical Oracle` — F1, F2 (single-voxel) | -| 11 | (c) vMF/Watson | `fzQuats.size() > 1` → `DirectionalStats::EMforDS` (VMF or WAT) → muhat, kappa; positiveOrientation; write quat/euler/kappa | `vMF/Watson EbsdLib Reference Oracle` (22-quat feature) + `Rodrigues Analytical Oracle` F3/F4 | -| 12 | (a) Preflight | all three methods disabled → preflight error `-54673` (runtime `-54670` kept as backstop, pinned by direct algorithm invocation) | `No Method Enabled Error` | -| 13 | (b)+(c) Guards | `Phases` value ≥ CrystalStructures tuple count → voxel excluded (no OOB read) + warning `-54672`| `Unknown Crystal Structure and Out-Of-Range Phase Guards` | -| 14 | (b) Rodrigues | crystal structure ≥ ops count (e.g. 999/Unknown) → voxel excluded + warning `-54671`; fully-excluded feature → identity | `Unknown Crystal Structure and Out-Of-Range Phase Guards` | -| 15 | (c) vMF/Watson | crystal structure ≥ ops count → feature skipped (outputs stay NaN) + warning `-54671` | `Unknown Crystal Structure and Out-Of-Range Phase Guards` | -| 16 | (c) vMF/Watson | gather loop excludes phase-0 / out-of-range-phase voxels (gate identical to counting pass; issue #1659) | `vMF/Watson Ignores Phase-0 Voxels` + guards test | +27 of 42 paths exercised. The table maps assertions on in-memory fixtures; external-sort provider contracts and defensive paths have separate gap rows. + +Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeAvgOrientations.cpp` (1499 lines) and `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeAvgOrientationsFilter.cpp` (296 lines). + +Test names below omit the common `OrientationAnalysis::ComputeAvgOrientations:` prefix. + +| # | Path | Test case | +|---|------|-----------| +| 1 | Rodrigues only; select its output tuple count | `Direct and Scanline method parity` | +| 2 | vMF only; select its output tuple count | `Direct and Scanline method parity` | +| 3 | Watson only; select its output tuple count | `Direct and Scanline method parity` | +| 4 | vMF and Watson enabled, Rodrigues disabled | `vMF/Watson EbsdLib Reference Oracle`; `Direct and Scanline method parity` | +| 5 | All three methods enabled | `Rodrigues Analytical Oracle`; `Direct and Scanline method parity` | +| 6 | Rodrigues and vMF enabled, Watson disabled | *Not directly tested. No fixture selects this pair.* | +| 7 | Rodrigues and Watson enabled, vMF disabled | *Not directly tested. No fixture selects this pair.* | +| 8 | No method enabled: preflight `-54673` and runtime `-54670` | `No Method Enabled Error` | +| 9 | Different cell-array tuple counts: `-651` | `Cell Array Tuple Mismatch Error (-651)` | +| 10 | Negative or excessive FeatureId: `-5355` or `-5351` | *Not directly tested. This file has no invalid-FeatureId fixture.* | +| 11 | Positive-phase cells contribute, including FeatureId zero | `Direct and Scanline method parity` | +| 12 | Nonpositive phases do not contribute | `Rodrigues Analytical Oracle`; `vMF/Watson Ignores Phase-0 Voxels` checks phase zero. | +| 13 | Positive phase outside the ensemble range: warning `-54672` | `Unknown Crystal Structure and Out-Of-Range Phase Guards` | +| 14 | Unsupported crystal structure: warning `-54671` | `Unknown Crystal Structure and Out-Of-Range Phase Guards` | +| 15 | First Rodrigues cell resets the reference; later cells accumulate | `Rodrigues Analytical Oracle`; `Rodrigues Voxel Ordering Independence` | +| 16 | Cubic symmetry and quaternion sign reduction | `Rodrigues Cubic Symmetry Invariant` | +| 17 | Normalize a populated Rodrigues average and convert it to Euler angles | `Rodrigues Analytical Oracle` | +| 18 | Empty Rodrigues feature gives identity and zero Euler angles | `Rodrigues Analytical Oracle` | +| 19 | Empty directional feature retains NaN outputs | `zero-cell and empty-feature outputs` | +| 20 | Single directional sample bypasses EM and gives kappa zero | `Rodrigues Analytical Oracle` | +| 21 | Multiple directional samples call VMF and WAT estimators | `vMF/Watson EbsdLib Reference Oracle` | +| 22 | Highest-index valid phase selects directional feature symmetry | `highest-index valid phase selects feature symmetry` | +| 23 | Zero input cells with allocated feature tuples | `zero-cell and empty-feature outputs` | +| 24 | Full 65,536-cell chunk and partial tail preserve tuple placement | `cross-chunk original tuple ordering parity` | +| 25 | Scanline directional gathering without an external-sort provider | `bounded fallback without external sort` | +| 26 | External-sort grouping, including feature flush and final flush | *Not directly tested. These fixtures do not require an external-sort provider.* | +| 27 | Bulk input-read failure propagates | `Scanline failures and cancellation` — `bulk read failure` | +| 28 | Bulk output-write failure propagates | `Scanline failures and cancellation` — `bulk write failure` | +| 29 | Cancellation at Scanline entry preserves outputs | `Scanline failures and cancellation` — `entry cancellation` | +| 30 | Cancellation after a Rodrigues input read preserves outputs | `Scanline failures and cancellation` — `mid-scan cancellation` | +| 31 | Cancellation between Rodrigues output writes stops the second write | `Scanline failures and cancellation` — `cancellation between output writes` | +| 32 | Direct entry, Rodrigues finalization or resident directional-worker cancellation | *Not directly tested. No fixture injects cancellation at these points.* | +| 33 | Directional counting, gathering, estimation or output-write cancellation | *Not directly tested. Cancellation fixtures select Rodrigues only.* | +| 34 | Scanline rejects missing, wrongly typed or malformed input arrays: `-54674`/`-54675` | *Not directly tested. Fixtures use valid input arrays or fail preflight first.* | +| 35 | Scanline rejects missing or malformed output arrays: `-54670`/`-54674`/`-54675` | *Not directly tested. No enabled-method fixture removes or reshapes an output.* | +| 36 | Count, component-product or buffer-size overflow: `-54676` | *Not directly tested. Requires synthetic size metadata or inconsistent buffers.* | +| 37 | Gathered count differs from the counting pass: `-54678` | *Not directly tested. Requires inconsistent grouped data.* | +| 38 | Sorter creation, append, finish or read failure; null sorter `-54679` | *Not directly tested. No test case installs the declared failing-sorter helper.* | +| 39 | External-sort record-count mismatch or short read: `-54680` | *Not directly tested. Requires an injected sorter contract failure.* | +| 40 | Invalid sorted FeatureId or original tuple index: `-54681` | *Not directly tested. Requires an injected malformed record.* | +| 41 | Sorted group has no selected operator or is out of order: `-54677`/`-54682` | *Not directly tested. Requires an injected malformed group.* | +| 42 | Original tuple indices are not strictly increasing: `-54683` | *Not directly tested. Requires duplicate or reordered sort records.* | ## Test inventory +All pass in CI. + | Test case | Status | Notes | |-----------|--------|-------| -| `OrientationAnalysis::ComputeAvgOrientations: Rodrigues Analytical Oracle` | new-for-V&V | Class 1 + Class 4. 6 Triclinic Rodrigues fixtures with closed-form averages (exact quats @1e-6) + invariants; also asserts vMF/Watson value-add invariants (single→FZ-quat & kappa 0, zero→NaN) on the same small data. Inline hand-built data. | -| `OrientationAnalysis::ComputeAvgOrientations: vMF/Watson EbsdLib Reference Oracle` | new-for-V&V | Class 2 + Class 4. One Cubic feature of EbsdLib's 22 reference quaternions reproduces EbsdLib `DirectionalStatsTest`'s documented VMF/WAT muhat+kappa (quat margin 5e-3, kappa 2% to absorb float32 input round-trip). Inline hand-built data. | -| `OrientationAnalysis::ComputeAvgOrientations: Rodrigues Cubic Symmetry Invariant` | new (issue #1660) | Class 4 under Cubic_High: five representations of one orientation (Rz(30+90k), −Rz(30)) must average to Rz(30). Exercises the symmetry-reduction branch under 24 operators. | -| `OrientationAnalysis::ComputeAvgOrientations: Rodrigues Voxel Ordering Independence` | new (issue #1661) | Class 4: F3 fixture in both voxel orderings → identical Rz(45). Encodes the invariant the provenance previously claimed without a test. | -| `OrientationAnalysis::ComputeAvgOrientations: vMF/Watson Ignores Phase-0 Voxels` | new (issue #1659) | Regression pin for the phase-gate fix: a phase-0 voxel inside a feature must not enter the gather (single-voxel shortcut preserved; EM path lands on the valid orientation; cross-checked vs Rodrigues). | -| `OrientationAnalysis::ComputeAvgOrientations: Unknown Crystal Structure and Out-Of-Range Phase Guards` | new (issue #1661) | Guards: unknown crystal structure (999) and out-of-range `Phases` are excluded from both paths with warnings `-54671`/`-54672` (asserted); dropped features → identity/NaN. | -| `OrientationAnalysis::ComputeAvgOrientations: No Method Enabled Error` | new-for-V&V (updated for #1661) | No method enabled → preflight error `-54673` (asserted); runtime backstop `-54670` pinned via direct algorithm invocation. | -| `OrientationAnalysis::ComputeAvgOrientations: Cell Array Tuple Mismatch Error (-651)` | new-for-V&V (updated for #1661) | Preflight error path: mismatched cell-array tuple counts → `-651` (code asserted). The duplicate TEST_CASE that PR #1644 added to this file was removed in favor of this one. | -| `OrientationAnalysis::ComputeAvgOrientationsFilter: SIMPL Backwards Compatibility` | kept | `DYNAMIC_SECTION` over SIMPL 6.4 + 6.5 conversion fixtures; validates UUID + argument-key conversion only. Unaffected by V&V. | - -**Coverage note (GCOV handoff, `.claude/gcov_ComputeAvgOrientations.md`):** the prior capture showed algorithm `.cpp` at 90.7% line / 46.5% branch, with the entire vMF/Watson path, the `-54670` branch, zero/single-voxel handling, and the VMF/Watson feature-array-detection branches **uncovered**. The V&V suite above now exercises all of them (Rodrigues, vMF, Watson, all-three-combined, both error paths, zero- and single-voxel features), closing the gap. The `-651` tuple branch (filter `.cpp`) is covered here; PR #1644's duplicate `-651` TEST_CASE was removed from this file (issue #1661). -| *(retired)* `OrientationAnalysis::ComputeAvgOrientations` | retired | Exemplar-comparison "valid execution" against `7_ComputeAvgOrientation_v2` (tol 5e-7). **Circular oracle** (exemplar regenerated from SIMPLNX output, PR #1577). Replaced by the Class 1/2/4 oracle tests above. | +| `OrientationAnalysis::ComputeAvgOrientations: Rodrigues Analytical Oracle` | new-for-V&V | Six feature tuples check identity, Rz90, Rz45, repeated orientations and empty features. Checks Euler invariants, directional single-sample shortcuts and NaN outputs. | +| `OrientationAnalysis::ComputeAvgOrientations: vMF/Watson EbsdLib Reference Oracle` | new-for-V&V | Checks both estimates for 22 cubic reference quaternions; component margin `5e-3`, kappa relative tolerance 2%. | +| `OrientationAnalysis::ComputeAvgOrientations: Rodrigues Cubic Symmetry Invariant` | new-for-V&V | Five symmetry/sign representations of Rz30 must average to Rz30 within `1e-5`. | +| `OrientationAnalysis::ComputeAvgOrientations: Rodrigues Voxel Ordering Independence` | new-for-V&V | Both `Ordering:` dynamic sections require the identity/Rz90 pair to average to Rz45. | +| `OrientationAnalysis::ComputeAvgOrientations: vMF/Watson Ignores Phase-0 Voxels` | new-for-V&V | Excludes invalid phase-zero samples from single-sample and EM features; checks D5. | +| `OrientationAnalysis::ComputeAvgOrientations: Unknown Crystal Structure and Out-Of-Range Phase Guards` | new-for-V&V | Checks warnings `-54671` and `-54672`, valid-feature values and excluded-feature identity/NaN values; checks D6 and D7. | +| `OrientationAnalysis::ComputeAvgOrientations: Direct and Scanline method parity` | kept | Five method combinations have independent single-orientation expectations. When both algorithms are selected, compares all enabled multi-sample output arrays. | +| `OrientationAnalysis::ComputeAvgOrientations: cross-chunk original tuple ordering parity` | kept | 65,538 cells put valid samples at indices 0, 65,536 and 65,537; checks their output feature tuples. Both-algorithm selection also compares multi-sample results. | +| `OrientationAnalysis::ComputeAvgOrientations: highest-index valid phase selects feature symmetry` | kept | Reverses two phases; expects Rz90 when the selected structure is valid, or NaNs and warning `-54671` when it is unknown. | +| `OrientationAnalysis::ComputeAvgOrientations: bounded fallback without external sort` | kept | Checks analytical outputs without a sorter and compares multi-sample results when both algorithms are selected. Returns early if a sorter is registered. | +| `OrientationAnalysis::ComputeAvgOrientations: Scanline failures and cancellation` | kept | Checks read/write error codes and unchanged output sentinels at three cancellation points. Assertions require selection of the Scanline scenario. | +| `OrientationAnalysis::ComputeAvgOrientations: zero-cell and empty-feature outputs` | kept | Zero cells and two features produce Rodrigues identity quaternions and six directional arrays filled with NaN. | +| `OrientationAnalysis::ComputeAvgOrientations: No Method Enabled Error` | new-for-V&V | Asserts preflight `-54673` and direct algorithm backstop `-54670`. | +| `OrientationAnalysis::ComputeAvgOrientations: Cell Array Tuple Mismatch Error (-651)` | new-for-V&V | Asserts preflight error `-651` for different cell-array tuple counts. | +| `OrientationAnalysis::ComputeAvgOrientationsFilter: SIMPL Backwards Compatibility` | kept | `SIMPL 6.5 (UUID)` and `SIMPL 6.4 (Filter_Name)` dynamic sections check converted UUID and argument values. | ## Exemplar archive -- **Archive:** `7_ComputeAvgOrientation_v2.tar.gz` -- **SHA512:** `2c2a691f1da301c449c20bafec65512d5134db38384ac7cb4c910880ccd87a260a5f011e905f35b97abff3952309f109c737c63ec3c833708926827a62a92efc` -- **Provenance:** `src/Plugins/OrientationAnalysis/vv/provenance/ComputeAvgOrientationsFilter.md` (documents the circular-oracle retirement and the inline replacement oracle) -- **Circular-oracle flag:** This `_v2` archive was regenerated from post-fix SIMPLNX output during PR #1577 (it bundles the filter's own `AvgQuats`, `Watson Avg Quats`, `vMF Avg Quats`, etc. as the comparison target). It is named explicitly in the audit's cross-cutting circular-oracle list. The V&V replaces this with oracle-derived expected values (Phase 10). - -## Deviations from DREAM3D 6.5.171 +- **Archive:** None in use. `7_ComputeAvgOrientation_v2.tar.gz` was retired. The tests now construct inputs and expected values in C++. +- **SHA512:** Not applicable; the current `src/Plugins/OrientationAnalysis/test/CMakeLists.txt` has no registration for this archive. The provenance sidecar retains the hash from its original registration. +- **Provenance:** `src/Plugins/OrientationAnalysis/vv/provenance/ComputeAvgOrientationsFilter.md` -*Comparison run 2026-06-30 against the **official DREAM3D 6.5.171 release** (`~/Applications/DREAM3D.app/Contents/bin/PipelineRunner`), Rodrigues method only (vMF/Watson have no legacy equivalent). Two byte-identical-input fixtures shared with NX `nxrunner`: **(A)** realistic — built from the `ASCIIData` CSVs (480k cells, 408 real features, single cubic phase); **(B)** a hand-built 3-feature fixture forcing the edge cases. **Result:** on fixture A the two are numerically equivalent on all 408 real features (`AvgQuats` ≤1e-6, zero sign flips; `AvgEulerAngles` max 4.77e-7); all divergences are confined to feature-0 / unindexed-voxel handling (D3 on A, D2 demonstrated on B). Full write-up: `vv/deviations/ComputeAvgOrientationsFilter.md` and the comparison summary in the archive (Phase 12).* +## Deviations from DREAM.3D v6.5.171 -- `ComputeAvgOrientationsFilter-D1` — **Downgraded — NOT a deviation.** Northern-hemisphere canonicalization (`getPositiveOrientation`) could not be made to diverge (symmetry reduction keeps `w≥0`; the pure-triclinic edge is `q≡−q`, same orientation). Documented so the source difference isn't re-flagged. See deviations file. -- `ComputeAvgOrientationsFilter-D2` — **Demonstrated:** FeatureId-0 voxels (phase>0) — SIMPLNX averages them, 6.5.171 writes `(0,0,0,0)`. Algorithmic choice (not a bug). See deviations file. -- `ComputeAvgOrientationsFilter-D3` — **Confirmed legacy bug:** empty feature 0 is `(0,0,0,0)` in 6.5.171 vs clean identity `(0,0,0,1)` in SIMPLNX. See deviations file. -- `ComputeAvgOrientationsFilter-D4` — **Confirmed sub-epsilon:** library/precision, `AvgEulerAngles` max diff 4.77e-7. See deviations file. +See `vv/deviations/ComputeAvgOrientationsFilter.md` for the root cause, affected users and recommendation. D1 remains in the sidecar as a retired candidate. -*Post-merge adversarial review of PR #1645 filed three follow-up issues, all resolved 2026-07-08 and reflected -in the Test inventory and Code path coverage above: **#1659** (SIMPLNX bug — vMF/Watson gather ignored `Phases`; -fixed and pinned by `vMF/Watson Ignores Phase-0 Voxels`), **#1660** (oracle gap — added `Rodrigues Cubic -Symmetry Invariant`), and **#1661** (range-checked phase index with warnings `-54671`/`-54672`; all-methods- -disabled promoted to preflight `-54673`; error tests assert their codes; removed PR #1644's duplicate `-651` -test).* +| Deviation | Observed difference | +|-----------|---------------------| +| `ComputeAvgOrientationsFilter-D2` | SIMPLNX averages positive-phase cells in feature zero; DREAM.3D v6.5.171 excludes them. | +| `ComputeAvgOrientationsFilter-D3` | SIMPLNX gives empty features identity quaternions; DREAM.3D v6.5.171 leaves tuple zero invalid and divides other empty features by zero. | +| `ComputeAvgOrientationsFilter-D4` | Different quaternion and Euler implementations produce float32 rounding differences. | diff --git a/src/Plugins/OrientationAnalysis/vv/ComputeCAxisLocationsFilter.md b/src/Plugins/OrientationAnalysis/vv/ComputeCAxisLocationsFilter.md index f01230f006..704e2193b3 100644 --- a/src/Plugins/OrientationAnalysis/vv/ComputeCAxisLocationsFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/ComputeCAxisLocationsFilter.md @@ -1,118 +1,114 @@ # V&V Report: ComputeCAxisLocationsFilter -| | | -|-----------|--------------------------| -| Plugin | OrientationAnalysis | +| | | +|--------|--------------| +| Plugin | OrientationAnalysis | | SIMPLNX UUID | `a51c257a-ddc1-499a-9b21-f2d25a19d098` | -| DREAM3D 6.5.171 equivalent | `FindCAxisLocations` (SIMPL UUID `68ae7b7e-b9f7-5799-9f82-ce21d0ccd55e`) - `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/FindCAxisLocations.{h,cpp}` | +| SIMPLNX Human Name | Compute C-Axis Locations | +| DREAM.3D v6.5.171 equivalent | `FindCAxisLocations` (SIMPL UUID `68ae7b7e-b9f7-5799-9f82-ce21d0ccd55e`) | | Verified commit | ** | -| Status | COMPLETE | -| Sign-off | Jared Duffey, 07-31-2026 | +| Status | COMPLETE | +| Sign-off | Jared Duffey, 2026-07-31; second engineer: Nathan Young, 2026-08-06 | ## At a glance -| Aspect | Current state | -|------------------------|--------------------------| -| Algorithm Relationship | Port - The EbsdLib, matrix math, and SIMPL APIs have changed but the code is functionally identical. Addition of several error branches when the crystal structure type is not hexagonal. | -| Oracle (confirmed) | **Class 1 (Analytical)** — 15 hand-derived orientation fixtures plus one 65,537-cell real-HDF5 page-tail fixture; all pass. | -| Code paths enumerated | 7 of 8 paths exercised - only the filter cancelation path is untested | -| Tests today | 7 registered cases cover analytical, mixed-phase, error, warning, bounds, and SIMPL-conversion behavior. One hidden OOC-only case verifies a 65,536-tuple page plus one-cell tail. | -| Exemplar archive | None - removed test using circular oracle data from `caxis_data.tar.gz` | -| Legacy comparison | Run 2026-07-31 against DREAM3D 6.5.171 using the 15 inline Class 1 fixtures and a shared serialized input. All 45 output float32 values were bit-identical, and the comparison artifacts were uploaded to OneDrive on 2026-07-31. | -| Bug flags | None | -| V&V phase | V&V is complete. OOC recertification now includes an output-sensitive real-HDF5 page-tail oracle. | - -For worked instances see `src/Plugins/OrientationAnalysis/vv/BadDataNeighborOrientationCheckFilter.md` and `src/Plugins/OrientationAnalysis/vv/ComputeAvgCAxesFilter.md` (on `topic/vv/compute_avg_caxis`). +| Aspect | Current state | +|--------|---------------| +| Algorithm Relationship | Port of `FindCAxisLocations` in DREAM.3D v6.5.171. SIMPLNX preserves the rotation equation and adds phase validation and non-hexagonal handling. | +| Oracle (confirmed) | Class 1 analytical vectors are encoded in 15 orientation fixtures and a mixed-phase companion in `test/ComputeCAxisLocationsTest.cpp`. All pass in CI. | +| Code paths enumerated | 11 of 20 exercised. Hexagonal-Low, phase zero, cancellation, empty input, chunk-boundary execution and transfer failures are outside the stated coverage. | +| Tests today | 7 ordinary test cases cover analytical values, mixed phases, diagnostics, phase bounds and SIMPL conversion. One additional hidden test defines a 65,537-cell boundary fixture. | +| Exemplar archive | None used by the current tests; inputs and expected values are defined in C++. `caxis_data.tar.gz` is retired for this filter. | +| Legacy comparison | Not run on the current algorithm. The available DREAM.3D v6.5.171 comparisons are dated 2026-07-31 and 2026-09-17; both precede the algorithm change. | +| Bug flags | None. | +| V&V phase | COMPLETE. | ## Summary -ComputeCAxisLocationsFilter determines the direction of the C-axis for each element, in the *sample reference frame*, by applying the quaternion of the element to the <001> direction, which is the C-axis for *Hexagonal* materials. - -The filter is verified with a Class 1 (Analytical) oracle. The filter uses the quaternion to rotate the C-axis into the sample reference frame. This is done by converting the quaternion to a rotation matrix. Then the transpose of the matrix is used due to DREAM3D conventions (see `wrapping/python/docs/source/Reference_Frame_Notes.md`). Due to the transpose the sign of the third element may need to be flipped. A new test was added with handed verified data. - -There were no deviations that affect the output found for hexagonal materials. +`ComputeCAxisLocationsFilter` computes the C-axis direction in the sample reference frame for each hexagonal cell. Class 1 analytical fixtures check the rotation, normalization, whole-vector sign reversal and mixed-phase NaN output. The filter now rejects invalid phase indices before the lookup. ## Algorithm Relationship -*Classification*: Port - -*Evidence*: Same loop with the same rotation equation used +*Classification:* Port -- UUID changed from SIMPL and filter renamed to match "Compute" naming conventions. -- EbsdLib is now up to version 3 which has equivalent but changed API for orientations like quaternions. The internal matrix math API has also changed here to use EbsdLib and Eigen but is functionally identical. -- Added one error branch before main execution checking for at least one hexagonal phase -- Added one warning branch before main execution for when not all phases are hexagonal -- Added branch inside loop to set not hexagonal quaternions to NAN to identify them as invalid - -*PR(s):* - -- **PR #576** ("FILTER: FindCAxisLocations & FindFeatureNeighborCAxisMisalignment filters") - Initial PR -- **PR #801** ("ENH: Rename complex to simplnx") - Library rename -- **PR #956** ("ENH: Rename Filters that start with Find/Generate/Calculate to Compute") - Filter rename -- **PR #1438** ("ENH: Microtexture related filter cleanup") - Header include changed -- **PR #1472** ("ENH: Update to EbsdLib 2.0.0 API") - EbsdLib 2.0.0 API update -- **PR #1582** ("ENH: Add missing cancel checks to lots of filters") - Added cancel check in loop +*Evidence:* DREAM.3D v6.5.171 `FindCAxisLocations::execute()` and SIMPLNX `ComputeCAxisLocations::operator()()` transpose the quaternion rotation matrix, apply it to `[0, 0, 1]`, normalize the vector and reverse all components when Z is negative. ## Oracle -*Class:* **Class 1 (Analytical)** +*Class:* 1 (Analytical). -*Applied:* Handed derived output of C-axis locations from quaternions. The expected outputs agree between DREAM3DNX, DREAM3D 6.5.171, and manual calculations (`v_passive ​= Rᵀv` with z component forced to positive). Includes 15 different orientations about x, y, and z at different angles. Using the previous formula, the exact form results were produced and compared against DREAM3D output. +*Applied:* Hand-derived rotations give 15 expected vectors, including an oblique rotation with three distinct components; the mixed-phase fixture requires analytical vectors for hexagonal cells and NaN for cubic cells. -*Encoded:* `test/ComputeCAxisLocationsTest.cpp::"OrientationAnalysis::ComputeCAxisLocationsFilter: Class 1 Oracle"` — 15 fixtures, all pass. `::"genuine HDF5 65536-page tail oracle"` verifies distinct analytical outputs at tuple indices 65,535 and 65,536 on actual HDF5 input/output stores. +*Encoded:* `src/Plugins/OrientationAnalysis/test/ComputeCAxisLocationsTest.cpp::OrientationAnalysis::ComputeCAxisLocationsFilter: Class 1 Oracle` — 15 fixtures; `Class 1 Oracle - Mixed hexagonal and non-hexagonal phases` reuses them with alternating phases. All pass in CI. -*Second-engineer review:* *Pending* +*Second-engineer review:* Nathan Young, 2026-08-06. ## Bugs found and fixed -None. +All defects in this table are fixed. The fixes will be in the DREAM3D-NX release after v7.4.2. -## Code path coverage +| Deviation | Defect | Affected released versions | Resolution | +|-----------|--------|----------------------------|------------| +| `ComputeCAxisLocationsFilter-D4` | A negative or out-of-range cell phase could cause an out-of-bounds read from Crystal Structures. | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was not affected. | The filter checks the phase before the lookup and returns error `-3524` for an invalid phase index. | -*7 of 8 paths exercised. The non-covered path is the cancellation branch which is not currently able to be tested for all filters* - -Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeCAxisLocations.cpp` (138 lines). +## Code path coverage -| # | Phase | Path | Test case| -|----|-----------------|---------------------------------------------------|--------------------------------------------| -| 1 | *Preflight* | Tuple validity check (-3520 error code) | "OrientationAnalysis::ComputeCAxisLocationsFilter: Preflight Error - Cell array tuple count mismatch (-3520)" | -| 2 | *Preflight* | Unconditional warning advising user to ensure their data contains hexagonal phases | "OrientationAnalysis::ComputeCAxisLocationsFilter: Not all hexagonal phases warning" | -| 3 | *Execute* | No hexagonal phases check (-3522 error code) | "OrientationAnalysis::ComputeCAxisLocationsFilter: No hexagonal phases error" | -| 4 | *Execute* | Not all phases hexagonal check (-3523 warning code) | "OrientationAnalysis::ComputeCAxisLocationsFilter: Not all hexagonal phases warning" | -| 5 | *Execute - per-cell* | Should cancel check | Not directly tested - no filter cancellation testing infrastructure | -| 6 | *Execute - per-cell* | Hexagonal C-axis location calculation path | "OrientationAnalysis::ComputeCAxisLocationsFilter: Class 1 Oracle" | -| 7 | *Execute - per-cell* | C-axis direction flip check | "OrientationAnalysis::ComputeCAxisLocationsFilter: Class 1 Oracle" | -| 8 | *Execute - per-cell* | Non-hexagonal NAN branch | "OrientationAnalysis::ComputeCAxisLocationsFilter: Not all hexagonal phases warning" | +11 of 20 paths exercised. Coverage below uses the ordinary test cases; each remaining path states its gap. + +Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeCAxisLocations.cpp` (138 lines) and `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeCAxisLocationsFilter.cpp` (147 lines). + +Test names below omit the common `OrientationAnalysis::ComputeCAxisLocationsFilter:` prefix. + +| # | Phase | Path | Test case | +|---|-------|------|-----------| +| 1 | Preflight | Cell tuple counts differ → error `-3520` | `Preflight Error - Cell array tuple count mismatch (-3520)` | +| 2 | Preflight | Valid tuple counts → create a three-component output and emit warning `-3521` | `Not all hexagonal phases warning`; `Class 1 Oracle` | +| 3 | Ensemble scan | No hexagonal entry after ensemble index zero → error `-3522` | `No hexagonal phases error` | +| 4 | Ensemble scan | Hexagonal and non-hexagonal entries coexist → warning `-3523` | `Not all hexagonal phases warning`; `Class 1 Oracle - Mixed hexagonal and non-hexagonal phases` | +| 5 | Chunk loop | Cancellation → return before the next chunk | *Not directly tested. No fixture injects cancellation.* | +| 6 | Per-cell calculation | Hexagonal-High → calculate and normalize the C-axis vector | `Class 1 Oracle` | +| 7 | Per-cell calculation | Z is negative → reverse all three components | `Class 1 Oracle` — 180° rotations about X and Y | +| 8 | Per-cell calculation | Non-hexagonal cell → write three NaNs | `Not all hexagonal phases warning`; `Class 1 Oracle - Mixed hexagonal and non-hexagonal phases` | +| 9 | Per-cell calculation | Hexagonal-Low → calculate and normalize the C-axis vector | *Not directly tested. All hexagonal fixtures use Hexagonal-High.* | +| 10 | Phase validation | Negative cell phase → error `-3524` | `Phase Index Bounds` — phase -1 | +| 11 | Phase validation | Cell phase reaches or exceeds the ensemble size → error `-3524` | `Phase Index Bounds` — phase 2 with two ensemble tuples | +| 12 | Phase validation | Phase zero → use Crystal Structures entry zero | *Not directly tested. Successful fixtures use positive cell phases.* | +| 13 | Per-cell calculation | Z is zero or positive → keep the vector sign | `Class 1 Oracle` — identity, 90° and 45° rotations | +| 14 | Bulk transfer | Read the ensemble cache and cell buffers; write a partial chunk | `Class 1 Oracle`; `Class 1 Oracle - Mixed hexagonal and non-hexagonal phases` | +| 15 | Chunk loop | Process a full 65,536-cell chunk and a one-cell tail | *Not directly tested. The ordinary fixtures do not cross a chunk boundary; the hidden boundary fixture requires HDF5 stores.* | +| 16 | Chunk loop | Zero cells → skip the loop | *Not directly tested. No fixture supplies empty cell arrays.* | +| 17 | Bulk transfer | Ensemble read fails → return the transfer error | *Not directly tested. No fixture injects an ensemble read failure.* | +| 18 | Bulk transfer | Quaternion read fails → return the transfer error | *Not directly tested. No fixture injects a quaternion read failure.* | +| 19 | Bulk transfer | Phase read fails → return the transfer error | *Not directly tested. No fixture injects a phase read failure.* | +| 20 | Bulk transfer | Output write fails → return the transfer error | *Not directly tested. No fixture injects an output write failure.* | ## Test inventory +The seven ordinary test cases run in the in-core builds on Linux, macOS and Windows. All pass in CI. + | Test case | Status | Notes | |-----------|--------|-------| -| "OrientationAnalysis::ComputeCAxisLocationsFilter: Valid Filter Execution" | retired | Circular oracle | -| "OrientationAnalysis::ComputeCAxisLocationsFilter: InValid Filter Execution" | retired | Superseded by more specific test "OrientationAnalysis::ComputeCAxisLocationsFilter: No hexagonal phases error" | -| "OrientationAnalysis::ComputeCAxisLocationsFilter: Preflight Error - Cell array tuple count mismatch (-3520)" | kept | Covers preflight error for tuple mismatch of phases and quats | -| "OrientationAnalysis::ComputeCAxisLocationsFilter: SIMPL Backwards Compatibility" | kept | Covers SIMPL json backwards compatibility | -| "OrientationAnalysis::ComputeCAxisLocationsFilter: No hexagonal phases error" | new-for-V&V | Covers no hexagonal phases branch | -| "OrientationAnalysis::ComputeCAxisLocationsFilter: Not all hexagonal phases warning" | new-for-V&V | Covers non-hexagonal branch | -| "OrientationAnalysis::ComputeCAxisLocationsFilter: Class 1 Oracle" | new-for-V&V | Covers hand calculated quaternions which also agree with DREAM3D 6.5.171. Also covers the sign flip path. | -| "OrientationAnalysis::ComputeCAxisLocationsFilter: Class 1 Oracle - Mixed hexagonal and non-hexagonal phases" | new-for-OOC recertification | Verifies exact analytical values for hexagonal cells and NaN output for non-hexagonal cells in one execution. | -| "OrientationAnalysis::ComputeCAxisLocationsFilter: Phase Index Bounds" | new-for-OOC recertification | Uses OOC-aware stores and verifies exact error `-3524` for negative and out-of-range phase indices. | -| "OrientationAnalysis::ComputeCAxisLocationsFilter: genuine HDF5 65536-page tail oracle" | new-for-OOC recertification | Hidden OOC-only case. It verifies actual HDF5 input/output stores and distinct literal c-axis values at the final tuple of a 65,536-tuple page and its one-cell tail. | - -All seven registered cases pass in both DREAM3D-NX builds. The hidden page-tail case passes in the OOC build with 25 assertions. - -OOC recertification, 2026-09-18: serial CTest passed 7/7 in `NX-Com-Qt69-Vtk96-Rel` and 7/7 in `NX-Com-Qt69-Vtk96-OoC-Rel`. The new hidden boundary case passed 25 assertions in the OOC binary and is included in the OOC-only `OrientationAnalysisOocStoreContracts` CTest entry. The original report status and sign-off above are historical and unchanged. +| `OrientationAnalysis::ComputeCAxisLocationsFilter: Preflight Error - Cell array tuple count mismatch (-3520)` | kept | Checks error `-3520` for 10 quaternion tuples and 9 phase tuples. | +| `OrientationAnalysis::ComputeCAxisLocationsFilter: No hexagonal phases error` | new-for-V&V | Checks execution error `-3522` when the only non-background ensemble is cubic. | +| `OrientationAnalysis::ComputeCAxisLocationsFilter: Not all hexagonal phases warning` | new-for-V&V | Checks warnings `-3521` and `-3523` and all 30 output components for NaN. | +| `OrientationAnalysis::ComputeCAxisLocationsFilter: Class 1 Oracle` | new-for-V&V | Checks all 45 components of 15 hand-derived vectors with `Approx(...).margin(1e-7f)`, including sign reversal. | +| `OrientationAnalysis::ComputeCAxisLocationsFilter: Class 1 Oracle - Mixed hexagonal and non-hexagonal phases` | new-for-V&V | Checks 24 analytical components for 8 hexagonal cells, 21 NaNs for 7 cubic cells and warning `-3523`. | +| `OrientationAnalysis::ComputeCAxisLocationsFilter: SIMPL Backwards Compatibility` | kept | Checks filter UUID, empty comments, quaternion path and output name in the `SIMPL 6.5 (UUID)` and `SIMPL 6.4 (Filter_Name)` dynamic sections. | +| `OrientationAnalysis::ComputeCAxisLocationsFilter: Phase Index Bounds` | new-for-V&V | Checks error `-3524` for phases -1 and 2 with two ensemble tuples (D4). | +| `OrientationAnalysis::ComputeCAxisLocationsFilter: genuine HDF5 65536-page tail oracle` | new-for-V&V | Hidden. Requires HDF5 input/output stores; checks distinct literal vectors at indices 65,535 and 65,536, output shape and warning codes. | ## Exemplar archive -No new exemplar archive was created for this V&V cycle: the Class 1 oracle is encoded entirely as inline expected values in the test source. Tests no longer use circular oracle `caxis_data.tar.gz`. - -## Deviations from DREAM3D 6.5.171 +- **Archive:** None used by the current tests. `caxis_data.tar.gz` is retired for this filter and remains registered in the plugin's `test/CMakeLists.txt`. +- **SHA512:** `56468d3f248661c0d739d9acd5a1554abc700bf136586f698a313804536916850b731603d42a0b93aae47faf2f7ee49d4181b1c3e833f054df6f5c70b5e041dc` (retired archive; matches `src/Plugins/OrientationAnalysis/test/CMakeLists.txt`). +- **Provenance:** Not applicable to the inline Class 1 fixtures; their inputs and expected values are in `src/Plugins/OrientationAnalysis/test/ComputeCAxisLocationsTest.cpp`. -The comparison was run on 2026-07-31 using the 15 quaternion fixtures from `"OrientationAnalysis::ComputeCAxisLocationsFilter: Class 1 Oracle"`. DREAM3D 6.5.171 serialized the shared input immediately before running `FindCAxisLocations`; the NX Debug pipeline read that same file and ran `ComputeCAxisLocationsFilter`. Nine shared input DataArrays remained identical in both outputs. The target arrays had identical float32 shape `(15, 3)`, 0 of 45 differing float32 words, and a maximum absolute NX-versus-6.5.171 difference of `0.0`; both outputs also satisfied the analytical oracle at absolute tolerance `1e-7`. +## Deviations from DREAM.3D v6.5.171 -The CSV fixture, shared input, legacy and NX pipelines, output DREAM3D files, and comparison summary were uploaded to OneDrive on 2026-07-31. +See `vv/deviations/ComputeCAxisLocationsFilter.md` for the root cause, affected users and recommendation for each Part 1 entry. -- D1 - non-hexagonal cells: NaN (NX) vs meaningless-but-finite computed value (6.5.171). NX intentional improvement; no legacy fix warranted. -- D2 - no hexagonal phases present: hard error -3522 (NX) vs silent full execution (6.5.171). -- D3 - added warnings -3521 (preflight, unconditional) and -3523 (execute, mixed phases); legacy emits none. +| Deviation | Observed difference | +|-----------|---------------------| +| `ComputeCAxisLocationsFilter-D1` | SIMPLNX writes NaN for non-hexagonal cells; DREAM.3D v6.5.171 computes a vector for every quaternion. | +| `ComputeCAxisLocationsFilter-D2` | SIMPLNX returns error `-3522` when no non-background ensemble is hexagonal; DREAM.3D v6.5.171 executes without that restriction. | +| `ComputeCAxisLocationsFilter-D3` | SIMPLNX emits preflight warning `-3521` and mixed-ensemble warning `-3523`; DREAM.3D v6.5.171 emits neither warning. | diff --git a/src/Plugins/OrientationAnalysis/vv/ComputeFaceIPFColoringFilter.md b/src/Plugins/OrientationAnalysis/vv/ComputeFaceIPFColoringFilter.md index 58310578e5..34777ed34a 100644 --- a/src/Plugins/OrientationAnalysis/vv/ComputeFaceIPFColoringFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/ComputeFaceIPFColoringFilter.md @@ -1,120 +1,117 @@ # V&V Report: ComputeFaceIPFColoringFilter -| | | -|-----------|--------------------------| -| Plugin | OrientationAnalysis | -| SIMPLNX UUID | 30759600-7c02-4650-b5ca-e7036d6b568e | -| DREAM3D 6.5.171 equivalent | GenerateFaceIPFColoring (legacy SIMPL UUID `0a121e03-3922-5c29-962d-40d88653f4b6`) | +| | | +|--------|--------------| +| Plugin | OrientationAnalysis | +| SIMPLNX UUID | `30759600-7c02-4650-b5ca-e7036d6b568e` | +| SIMPLNX Human Name | Compute IPF Colors (Face) | +| DREAM.3D v6.5.171 equivalent | `GenerateFaceIPFColoring` (SIMPL UUID `0a121e03-3922-5c29-962d-40d88653f4b6`) | | Verified commit | ** | -| Status | COMPLETE | -| Sign-off | Michael Jackson — 2026-07-16 | +| Status | COMPLETE | +| Sign-off | Michael Jackson, 2026-06-30; second engineer: Nathan Young, 2026-07-10 | ## At a glance -| Aspect | Current state | -|------------------------|--------------------------| -| Algorithm Relationship | **Port** of legacy `GenerateFaceIPFColoring` — per-face IPF color math is line-for-line identical. Deltas: output split from one 6-component array into two 3-component arrays, an added Color Key choice (TSL/PUCM/Nolze-Hielscher), and the EbsdLib 3.0.0 API. The 2023 wrong-phase bug (issue #1635) was ported verbatim and is **fixed here**. | -| Oracle (confirmed) | **Class 1 (Analytical)** + Class 4 companion — closed-form IPF corner colors on a hand-built mixed cubic/hex mesh. Encoded as `Class 1 Oracle - mixed-phase analytical` and the real-HDF5 65,536-block tail oracle; both pass. | -| Code paths enumerated | 13 of 16 exercised; 1 is unreachable dead code (`-2431`) and 2 are defensive guards (out-of-range crystal-structure index, out-of-range color key) noted below. | -| Tests today | 5 registered test cases plus 1 hidden OOC contract test — analytical output, phase bounds, negative validation, Color-Key plumbing, SIMPL conversion, and a 65,536-block real-HDF5 tail witness. | -| Exemplar archive | **None — analytical fixtures inlined.** The prior `Valid filter execution` test compared against a `SurfaceMeshFaceIPFColors` array baked into the shared `6_6_Small_IN100_GBCD.tar.gz`; that comparison was a circular oracle (filter's own pre-fix output) and is **retired**. | -| Legacy comparison | **Run** — SIMPLNX vs DREAM3D 6.5.171 on a hand-built cubic/hex mesh and a 756,474-face real mesh found 2 deviations. A patched local legacy build confirms the D1 root cause. Cubic coloring is identical. | -| Bug flags | `…-D1` — wrong-phase Laue operator on the Phase-2 side (a 6.5.171 bug, fixed). `…-D2` — hex basal IPF hue differs between EbsdLib (NX, green) and legacy EbsdLib (blue); resolved as a library convention difference — EbsdLib is canonical (3.0.0 and 3.1.0 agree), legacy is the deviation, trust SIMPLNX. Affects all hex IPF coloring. | -| V&V phase | The original COMPLETE status and 2026-07-16 sign-off are retained. OOC recertification adds the analytical HDF5 tail witness and paired CTest checks. The optional documentation image remains outside this work. | +| Aspect | Current state | +|--------|---------------| +| Algorithm Relationship | Minor changes to `GenerateFaceIPFColoring` in DREAM.3D v6.5.171. SIMPLNX separates the two RGB outputs, adds color keys and corrects phase handling. | +| Oracle (confirmed) | Classes 1 and 4: exact TSL corner colors and a zero-red basal-plane invariant. One five-face fixture in `test/ComputeFaceIPFColoringTest.cpp` encodes these checks. All pass in CI. | +| Code paths enumerated | 15 of 25 exercised by the mapped ordinary tests. Gaps include tuple-count guards, invalid choices, read failures and unsupported phase or symmetry inputs. | +| Tests today | Six test cases: five ordinary cases and one hidden boundary case. They cover analytical colors, phase bounds, invalid selections, color-key routing and SIMPL conversion. | +| Exemplar archive | `6_6_Small_IN100_GBCD.tar.gz` supplies inputs for validation and color-key tests. The archived face colors are no longer a correctness oracle. | +| Legacy comparison | Not run on the current algorithm. The available comparison is dated 2026-06-30; algorithm source changed after that run. | +| Bug flags | None | +| V&V phase | COMPLETE. The five ordinary test cases cover the analytical oracle and both fixed defects. All pass in CI. | ## Summary -`ComputeFaceIPFColoringFilter` assigns an IPF color to each side of every surface-mesh triangle. A Class 1 analytical oracle and a real-HDF5 65,536-block tail oracle verify the output. SIMPLNX matches the analytical oracle, and the documented DREAM3D 6.5.171 deviations remain reconciled. +`ComputeFaceIPFColoringFilter` calculates an IPF color for each side of a surface-mesh face. A five-face analytical fixture and a basal-plane invariant check the corrections for wrong-phase coloring; phase-bounds tests check the correction for out-of-range phase access. All pass in CI. ## Algorithm Relationship -**Port** +*Classification:* Minor changes -*Evidence:* SIMPLNX inherits the legacy SIMPL UUID `0a121e03-3922-5c29-962d-40d88653f4b6` (see `OrientationAnalysisLegacyUUIDMapping.hpp`). The per-face coloring kernel (`CalculateFaceIPFColorsImpl::generate`) is a line-for-line translation of legacy `GenerateFaceIPFColoring.cpp`. - -*Port-time deltas (each assessed for output impact):* - -1. **Output layout** — legacy wrote a single 6-component `SurfaceMeshFaceIPFColors` array (`m_Colors[6*i+0..5]`); SIMPLNX writes two separate 3-component arrays (`FirstFaceIPFColors`, `SecondFaceIPFColors`). Pure repackaging — the six bytes per face are identical, only regrouped. No value change. -2. **Color Key parameter** — SIMPLNX adds a TSL/PUCM/Nolze-Hielscher choice routed into `generateIPFColor(..., m_ColorKey)`; legacy always used TSL. Default (TSL) reproduces legacy output exactly; the new option is additive. -3. **EbsdLib API** — `Ebsd::` → `ebsdlib::`, and `generateIPFColor` gained the `ColorKeyKind` argument. EbsdLib upgraded to 3.0.0. No change to the TSL color math at the standard-triangle corners used by the oracle. -4. **Parallelization** — legacy used TBB-style parallel-for over faces. SIMPLNX structures the loop with `ParallelDataAlgorithm` but **parallelization is disabled**: the worker writes `UInt8Array` outputs via `operator[]`, and per project thread-safety policy DataArray/DataStore access is not thread-safe even for distinct indices (same disposition as the `ComputeFeatureFaceMisorientations` V&V). No output impact. -5. **Wrong-phase bug** — the Phase-2 branch's `m_CrystalStructures[phase1]` guard and operator lookup were ported from the legacy code. **Fixed here** (`phase1`→`phase2`). A patched local legacy build confirms the root cause. - -*Material PRs since baseline:* #1631 (EbsdLib 3.0.0 + V&V cohort) added the Color Key option and first documented this bug with a `// KNOWN BUG` block; this V&V cycle removes that block and applies the fix. +*Evidence:* `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/OrientationAnalysisLegacyUUIDMapping.hpp` maps the legacy UUID to this filter; the same per-side orientation calculation now uses separate RGB arrays, selectable color keys and validated phase indices. ## Oracle -*Class:* **1 (Analytical)**, with a **Class 4 (Invariant)** companion. - -*Applied:* IPF color is a closed-form function of (orientation, reference direction, Laue symmetry). At the standard stereographic-triangle corners the color is a pure primary, independent of implementation: cubic-high `<100>`→(255,0,0), cubic-high `<111>`→(0,0,255), hex-high c-axis→(255,0,0), hex-high basal→(0,255,0) (red channel exactly 0, since a basal direction sits at χ = χ_max so r = 1 − χ/χ_max = 0). A 5-face hand-built mesh with corner-aligned normals (Phase 1 = cubic, Phase 2 = hex) yields fully hand-derivable expected colors for both sides of every face, and **all four** corner primaries are exercised (face 4 pins the hex c-axis red corner). Every feature carries a **distinct orientation** chosen so the corner colors remain hand-derivable (cubic: 90° about Z maps `<100>`→`<100>`, `<111>`→`<111>`; hex: 60° about c is a 6/mmm symmetry operation) — so a wrong-Euler-index defect (the Euler analogue of the #1635 phase-index bug) changes the output and fails the exact-value assertions. The Class 4 companion asserts the crispest bug signature: the hex (Phase-2) side of a basal face has red channel == 0, whereas the bug's cubic `<100>` lookup gives 255. +*Class:* 1 (Analytical), with 4 (Invariant). -*Encoded:* `test/ComputeFaceIPFColoringTest.cpp::"OrientationAnalysis::ComputeFaceIPFColoringFilter: Class 1 Oracle - mixed-phase analytical"` — 5 faces × (3 first + 3 second) = 30 color-byte assertions + 1 invariant assertion, all pass. Verified to **fail** (a) on Face 0's second-color red channel when the `phase1`→`phase2` fix is reverted, and (b) on the hex-side exact values when the Phase-2 branch is mutated to read feature1's Euler angles (wrong-Euler-index mutation check, run 2026-07-08). The hidden `genuine HDF5 65536-block tail oracle` uses HDF5-OOC inputs and outputs. It checks distinct exact RGB values at tuple 65,535 and the one-face partial tail at tuple 65,536. It passes with 38 assertions. +*Applied:* Hand-derived TSL corner colors specify both RGB arrays on five cubic/hexagonal faces with distinct feature orientations; the hexagonal basal direction must have a zero red channel. -*Caveat (surfaced by the legacy A/B):* the exact hex basal value `(0,255,0)` green is the **EbsdLib** assignment (canonical); legacy DREAM3D's older EbsdLib assigns the other basal corner (blue) to the same direction (deviation `-D2`). The convention-independent part of the oracle is the **red channel == 0** invariant. Both EbsdLib generations satisfy this invariant. The exact green is confirmed under EbsdLib 3.0.0 and 3.1.0. +*Encoded:* `src/Plugins/OrientationAnalysis/test/ComputeFaceIPFColoringTest.cpp::OrientationAnalysis::ComputeFaceIPFColoringFilter: Class 1 Oracle - mixed-phase analytical` — one five-face fixture, 30 exact color-byte checks and one invariant. All pass in CI. -*Second-engineer review:* **Signed off by Michael Jackson (technical authority), 2026-07-16.** +*Second-engineer review:* Nathan Young, 2026-07-10. ## Bugs found and fixed -This branch fixes all defects in this table. The fix will be in the DREAM3D-NX release after v7.4.1. +All defects in this table are fixed. The fixes will be in the DREAM3D-NX release after v7.4.2. DREAM3D-NX v7.4.2 contains the fix for `ComputeFaceIPFColoringFilter-D1`. -| Deviation | Defect | Affected released versions | Resolution in this branch | -|-----------|--------|----------------------------|---------------------------| -| `ComputeFaceIPFColoringFilter-D1` | The second face side used the first side's Laue operator. Mixed-phase colors were wrong, and some exterior sides stayed black. | DREAM.3D 6.5.171; DREAM3D-NX v7.0.0 through v7.4.1. | The second side now uses its own phase index. The analytical oracle verifies the correction. | +| Deviation | Defect | Affected released versions | Resolution | +|-----------|--------|----------------------------|------------| +| `ComputeFaceIPFColoringFilter-D1` | The second face side used the first side's symmetry operator. Mixed-phase colors were wrong, and some exterior sides stayed black. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | Each side uses its own phase and orientation. The five-face oracle checks exact colors. | +| `ComputeFaceIPFColoringFilter-D3` | A referenced positive phase outside the ensemble range caused an out-of-bounds read. | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was affected. | Validation returns `-24341` before coloring either side; it ignores unreferenced feature phases. | ## Code path coverage -13 of 16 paths exercised (1 unreachable dead guard, 2 defensive guards untested). - -Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeFaceIPFColoring.cpp` (265 lines), plus `ComputeFaceIPFColoringFilter.cpp` preflight/execute. Logical phases: (a) preflight validation, (b) execute color-key routing, (c) per-face label→phase resolution, (d) Phase-1 first-color, (e) Phase-2 second-color. - -| # | Phase | Path | Test case | -|----|------------------|-----------------------------------------------------------------------------------|------------------------------------------------------------------------| -| 1 | (a) Preflight | face label/normal tuple mismatch → error `-2430`| `Invalid filter execution` — "Inconsistent face data tuple dimensions" | -| 2 | (a) Preflight | feature euler/phase tuple mismatch → error `-2432` | `Invalid filter execution` — "Inconsistent cell data tuple dimensions" | -| 3 | (a) Preflight | face labels array missing → error `-2431` | *Unreachable dead code: the path is an `ArraySelectionParameter`, which guarantees existence before `preflightImpl` runs, and the `-2430` tuple validation would fail first anyway.* | -| 4 | (b) Execute | Color Key 0/1/2 → TSL/PUCM/Nolze-Hielscher | `ColorKey choice reaches algorithm` — all three kinds | -| 5 | (b) Execute | Color Key out of [0,2] → error `-24340` | *Not directly tested. `ChoicesParameter` constrains the index; low-value.* | -| 6 | (c) Per-face | `feature1 > 0` → `phase1 = m_Phases[feature1]` | `Class 1 Oracle` — faces 0, 2, 3 | -| 7 | (c) Per-face | `feature1 <= 0` → `phase1 = 0` | `Class 1 Oracle` — face 1 (label −1)| -| 8 | (c) Per-face | `feature2 > 0` / `feature2 <= 0` → `phase2` set or 0 | `Class 1 Oracle` — faces 0/1 (valid), face 2 (−1) | -| 9 | (d) First color | `phase1 > 0` and crystal structure valid → own-phase IPF color | `Class 1 Oracle` — faces 0, 2 (cubic red), face 3 (cubic blue), face 4 (hex c-axis red) | -| 10 | (d) First color | `phase1 <= 0` → first color black | `Class 1 Oracle` — face 1 → (0,0,0) | -| 11 | (e) Second color | `phase2 > 0` and `CrystalStructures[phase2]` valid → **own-phase** IPF color (fix) | `Class 1 Oracle` — face 0 hex green, face 1 hex green, face 3 blue | -| 12 | (e) Second color | `phase2 <= 0` → second color black | `Class 1 Oracle` — face 2 → (0,0,0) | -| 13 | (d/e) | phase valid but `CrystalStructures[phase] >= LaueGroupEnd` → color left untouched | *Not directly tested. Low-value guard for a corrupt crystal-structure index.* | -| 14 | Validation | Referenced feature phase is outside the ensemble range | `Phase Index Bounds` — first and second feature sections | -| 15 | Validation | Invalid unreferenced phase is ignored | `Phase Index Bounds / Unreferenced Feature Phase is ignored` | -| 16 | Boundary | A 65,536-face block and one-face tail produce different colors | `genuine HDF5 65536-block tail oracle` — literal red/black and black/green sides | - +15 of 25 paths exercised. Each uncovered path has a reason below. + +Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeFaceIPFColoring.cpp` (265 lines) and `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeFaceIPFColoringFilter.cpp` (216 lines). + +Test names below omit `OrientationAnalysis::ComputeFaceIPFColoringFilter:`. The negative selection test fails parameter validation before the two filter-specific tuple-count guards. + +| # | Phase | Path | Test case | +|---|-------|------|-----------| +| 1 | Preflight | Reject selected arrays with incompatible types or component shapes | `Invalid filter execution` — both sections | +| 2 | Preflight | Face Labels and Face Normals have different tuple counts → `-2430` | *Not directly tested. The negative fixture selects an invalid normal type and shape before this guard.* | +| 3 | Preflight | Missing Face Labels → `-2431` | *Not directly tested. Array-selection validation rejects the missing array first.* | +| 4 | Preflight | Feature Euler Angles and Feature Phases have different tuple counts → `-2432` | *Not directly tested. The negative fixture selects a float64 phases array before this guard.* | +| 5 | Preflight | Create two uint8 RGB arrays with the face tuple shape | `Class 1 Oracle - mixed-phase analytical` | +| 6 | Routing | Color Key 0 selects TSL | `Class 1 Oracle - mixed-phase analytical` — exact RGB values | +| 7 | Routing | Color Key 1 selects PUCM | `ColorKey choice reaches algorithm` — first-side array differs from TSL; no independent PUCM values | +| 8 | Routing | Color Key 2 selects Nolze-Hielscher | `ColorKey choice reaches algorithm` — first-side array differs from TSL; no independent Nolze-Hielscher values | +| 9 | Routing | Invalid color-key index → `-24340` | *Not directly tested. ChoicesParameter rejects an invalid index before execution.* | +| 10 | Validation | Propagate a Feature Phases bulk-read failure | *Not directly tested. Requires an injected store read failure.* | +| 11 | Validation | Propagate a Face Labels bulk-read failure | *Not directly tested. Requires an injected store read failure.* | +| 12 | Validation | Skip non-positive face labels | `Class 1 Oracle - mixed-phase analytical`; `Phase Index Bounds` | +| 13 | Validation | Accept referenced positive phases within the ensemble range | `Class 1 Oracle - mixed-phase analytical` | +| 14 | Validation | Reject an out-of-range phase on the first side → `-24341` | `Phase Index Bounds` / `First Feature Phase returns an error` | +| 15 | Validation | Reject an out-of-range phase on the second side → `-24341` | `Phase Index Bounds` / `Second Feature Phase returns an error` | +| 16 | Validation | Ignore an invalid phase on an unreferenced feature | `Phase Index Bounds` / `Unreferenced Feature Phase is ignored` | +| 17 | First color | Use the first feature's phase, Euler angles and face normal | `Class 1 Oracle - mixed-phase analytical` — cubic red/blue and hexagonal c-axis red | +| 18 | First color | Non-positive first label gives phase zero and black | `Class 1 Oracle - mixed-phase analytical` — face 1 | +| 19 | First color | Positive phase has an unsupported Laue index → skip color write | *Not directly tested. No fixture assigns an unsupported structure to a referenced positive phase.* | +| 20 | Second color | Use the second feature's phase, Euler angles and reversed normal | `Class 1 Oracle - mixed-phase analytical` — hexagonal green and cubic red/blue | +| 21 | Second color | Non-positive second label gives phase zero and black | `Class 1 Oracle - mixed-phase analytical` — face 2 | +| 22 | Second color | Positive phase has an unsupported Laue index → skip color write | *Not directly tested. No fixture assigns an unsupported structure to a referenced positive phase.* | +| 23 | Both colors | Positive feature label maps to a zero or negative phase → black | *Not directly tested. The black-color fixtures use non-positive feature labels.* | +| 24 | Validation | Read full 65,536-face blocks and a partial final block | `ColorKey choice reaches algorithm` — the 756,474-face input spans both block sizes | +| 25 | Loops | Empty face array skips validation and coloring loops | *Not directly tested. All fixtures contain faces.* | ## Test inventory | Test case | Status | Notes | |-----------|--------|-------| -| `Class 1 Oracle - mixed-phase analytical` | new-for-V&V | Hand-built 5-face cubic/hex mesh with distinct per-feature orientations; 30 color-byte + 1 invariant assertions; analytical corner-primary oracle covering all four corners. Verified to fail under both the phase-index and Euler-index mutations. | -| `Phase Index Bounds` | kept | Uses HDF5-OOC stores and checks both referenced-feature phase bounds plus the valid unreferenced-feature case. | -| `genuine HDF5 65536-block tail oracle` | new-for-V&V | Hidden OOC contract test. Uses HDF5-OOC inputs and outputs and checks exact cubic-red and hex-green RGB values at tuple 65,535 and the one-face partial tail at tuple 65,536. Passes with 38 assertions. | -| `Valid filter execution` | retired | Compared against `SurfaceMeshFaceIPFColors` baked into `6_6_Small_IN100_GBCD` — the filter's own pre-fix output (circular oracle). It encoded the bug on `feature1`-invalid boundary faces, so the fix correctly broke it. Superseded by the Class 1 analytical test; real-data exercise retained by the Color-Key test below. | -| `Invalid filter execution` | kept | Two preflight tuple-mismatch sections (`-2430`, `-2432`). | -| `ColorKey choice reaches algorithm` | kept | Runs the filter on the full Small IN100 GBCD surface mesh three times (TSL/PUCM/NH) and asserts the outputs differ — real-data smoke test independent of any baked exemplar. | -| `SIMPL Backwards Compatibility` | kept | 6.4 and 6.5 SIMPL JSON → Arguments round-trip. | - -OOC recertification, 2026-09-18: serial CTest passed 5/5 in `NX-Com-Qt69-Vtk96-Rel` and 5/5 in `NX-Com-Qt69-Vtk96-OoC-Rel`. The new hidden boundary case passed 38 assertions in the OOC binary and is included in the OOC-only `OrientationAnalysisOocStoreContracts` CTest entry. The original report status and sign-off above are historical and unchanged. +| `OrientationAnalysis::ComputeFaceIPFColoringFilter: Class 1 Oracle - mixed-phase analytical` | new-for-V&V | Checks 30 RGB bytes and one zero-red invariant on five faces with distinct feature orientations. Covers D1 and the current hexagonal hue convention. | +| `OrientationAnalysis::ComputeFaceIPFColoringFilter: Phase Index Bounds` | kept | Checks `-24341` for a referenced invalid phase on each side and success for an invalid unreferenced phase. Covers D3. | +| `OrientationAnalysis::ComputeFaceIPFColoringFilter: genuine HDF5 65536-block tail oracle` | new-for-V&V | Hidden; requires HDF5 stores. Asserts red/black at tuple 65,535 and black/green at tuple 65,536, plus store formats and tuple dimensions. | +| `OrientationAnalysis::ComputeFaceIPFColoringFilter: Invalid filter execution` | kept | Two sections assert invalid preflight and execution for incompatible selected arrays. They do not assert `-2430` or `-2432`. | +| `OrientationAnalysis::ComputeFaceIPFColoringFilter: ColorKey choice reaches algorithm` | kept | Runs all three color keys on Small IN100. Checks equal first-side array sizes and that each nondefault first-side array differs from TSL. | +| `OrientationAnalysis::ComputeFaceIPFColoringFilter: SIMPL Backwards Compatibility` | kept | `SIMPL 6.5 (UUID)` and `SIMPL 6.4 (Filter_Name)` sections check UUID, comments, five input paths and the first output name. | ## Exemplar archive -- **Archive:** None — Class 1 analytical fixtures are inlined in the test source. -- **SHA512:** n/a -- **Provenance:** No sidecar required (no archive owns the oracle). The retired circular-oracle situation is documented in `vv/provenance/ComputeFaceIPFColoringFilter-circular-oracle.md`. - -## Deviations from DREAM3D 6.5.171 - -Three-way binary comparison (6.5.171 / local patched legacy build / SIMPLNX) on a hand-built legacy-native cubic/hex mesh, plus SIMPLNX-vs-baked on the 756,474-face real mesh. +- **Archive:** `6_6_Small_IN100_GBCD.tar.gz` +- **SHA512:** `543e3bdcee24ff9e5cd80dfdedc39ceef1529e7172cebd01d8e5518292ffdf4e0eb2e79d75854cb3eaca5c60e19c861ca67f369e21b81c306edb66327f47a1e3` +- **Provenance:** `src/Plugins/OrientationAnalysis/vv/provenance/ComputeFaceIPFColoringFilter-circular-oracle.md` -> **Evidence (reproducible from OneDrive archive):** the A/B **fixture builder and comparison pipelines** are archived in the OneDrive verification archive (`vv_work/face_ipf/`); the measured figures below are **reproducible by re-running those generators + pipelines** against DREAM3D 6.5.171 / local patched legacy build / SIMPLNX. The rendered output snapshots and the prose `legacy_comparison_summary.md` write-up were not preserved, so the byte-level figures are reproducible rather than directly archived as result files. This A/B is **corroborating, not load-bearing**: the `phase1`→`phase2` fix is independently verified by the Class 1 analytical oracle (mutation-tested), which is the primary correctness evidence. +## Deviations from DREAM.3D v6.5.171 -- `ComputeFaceIPFColoringFilter-D1` — Phase-2 face side colored with Phase-1's Laue symmetry operator (and left black on `feature1`-invalid boundary faces). The #1635 bug; 6.5.171 reproduces it; SIMPLNX and a patched local legacy build correct it. 120,000/756,474 faces affected on real data (reproducible from the archived A/B generators). See `vv/deviations/ComputeFaceIPFColoringFilter.md`. -- `ComputeFaceIPFColoringFilter-D2` — hex basal IPF hue differs between EbsdLib 3.0.0 (SIMPLNX, green) and legacy EbsdLib (6.5.171 and the patched local build, blue); a library deviation affecting all hex IPF coloring, flagged for review. +See `vv/deviations/ComputeFaceIPFColoringFilter.md` for the root cause, affected users and recommendation for each difference. Only Part 1 entries appear here. -*Note:* SIMPLNX-written `.dream3d` files are, by design, not readable by legacy DREAM3D 6.5.171 (the FileVersion dataset gates this), so the legacy A/B input was authored directly in legacy format with the `legacy_dream3d` writer — the standard approach for legacy comparisons. +| Deviation | Observed difference | +|-----------|---------------------| +| `ComputeFaceIPFColoringFilter-D1` | SIMPLNX uses each side's own symmetry operator; DREAM.3D v6.5.171 uses the first phase on the second side. | +| `ComputeFaceIPFColoringFilter-D2` | The tested hexagonal basal direction is green in SIMPLNX and blue with the legacy library after correction of D1 in a local proof version. | +| `ComputeFaceIPFColoringFilter-D4` | SIMPLNX writes two three-component RGB arrays; DREAM.3D v6.5.171 writes one six-component array. | +| `ComputeFaceIPFColoringFilter-D5` | SIMPLNX adds PUCM and Nolze-Hielscher color keys; DREAM.3D v6.5.171 uses TSL only. | diff --git a/src/Plugins/OrientationAnalysis/vv/ComputeFeatureFaceMisorientationFilter.md b/src/Plugins/OrientationAnalysis/vv/ComputeFeatureFaceMisorientationFilter.md new file mode 100644 index 0000000000..e157a80914 --- /dev/null +++ b/src/Plugins/OrientationAnalysis/vv/ComputeFeatureFaceMisorientationFilter.md @@ -0,0 +1,109 @@ +# V&V Report: ComputeFeatureFaceMisorientationFilter + +| | | +|--------|--------------| +| Plugin | OrientationAnalysis | +| SIMPLNX UUID | `f3473af9-db77-43db-bd25-60df7230ea73` | +| SIMPLNX Human Name | Compute Feature Face Misorientation (Face) | +| DREAM.3D v6.5.171 equivalent | `GenerateFaceMisorientationColoring` | +| Verified commit | ** | +| Status | COMPLETE | +| Sign-off | Nathan Young, 2026-05-19; second engineer: Michael Jackson, 2026-05-28 | + +## At a glance + +| Aspect | Current state | +|--------|---------------| +| Algorithm Relationship | Rewrite of `GenerateFaceMisorientationColoring` in DREAM.3D v6.5.171. SIMPLNX returns one angle, supports all 11 Laue classes and writes NaN for faces without a computed value. | +| Oracle (confirmed) | Class 1 analytical expectations in `test/ComputeFeatureFaceMisorientationTest.cpp` check 33 angles and four NaN values. | +| Code paths enumerated | 9 of 19 exercised by the ordinary tests. Cancellation, defensive guards, I/O failures and the full validation-block boundary have no direct ordinary-test coverage. | +| Tests today | Three ordinary cases cover analytical values, phase bounds and SIMPL conversion. One hidden case targets a 65,536-face block boundary. | +| Exemplar archive | None. The tests construct their inputs in C++. | +| Legacy comparison | Not run on the current algorithm. The archived comparison predates the phase-bounds validation and lookup changes. | +| Bug flags | None | +| V&V phase | COMPLETE. | + +## Summary + +`ComputeFeatureFaceMisorientationFilter` computes the symmetry-reduced misorientation angle between the features on each mesh face. Class 1 expectations check all 11 Laue classes and distinguish missing values from zero angles. The filter includes the cubic precision and phase-bounds fixes below. + +## Algorithm Relationship + +*Classification:* Rewrite + +*Evidence:* SIMPLNX replaces the three-component axis-times-angle output with one angle; `OrientationAnalysisLegacyUUIDMapping.hpp` maps the legacy UUID to this filter. + +## Oracle + +*Class:* 1 (Analytical). + +*Applied:* Pure c-axis rotations of 45°, 90° and 180° give three symmetry-reduced angles for each of 11 Laue classes; two background and two mixed-phase faces must give NaN. + +*Encoded:* `src/Plugins/OrientationAnalysis/test/ComputeFeatureFaceMisorientationTest.cpp::OrientationAnalysis::ComputeFeatureFaceMisorientationFilter: Curated Data` — 37 face expectations, with absolute angle error below 0.000012°. All pass in CI. + +*Second-engineer review:* Michael Jackson, 2026-05-28, as recorded in the report sign-off. + +## Bugs found and fixed + +All defects in this table are fixed. The fixes will be in the DREAM3D-NX release after v7.4.2. DREAM3D-NX v7.4.2 contains the fix for `ComputeFeatureFaceMisorientations-D4`. + +| Deviation | Defect | Affected released versions | Resolution | +|-----------|--------|----------------------------|------------| +| `ComputeFeatureFaceMisorientations-D4` | Cubic angle extraction loses precision near zero; the SIMPLNX 90° symmetry fixture gave a false residual of about 0.0212°. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected by precision loss in its float calculation. | The filter computes cubic angles through EbsdLib with explicit reduced-quaternion components and `2*atan2` angle extraction. | +| `ComputeFeatureFaceMisorientationFilter-D5` | A positive phase outside Crystal Structures can cause an out-of-bounds read. | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was affected. | The filter validates participating phases before the lookup, returns `-98412` for an invalid shared phase and skips mismatched phases. | + +## Code path coverage + +9 of 19 paths exercised. The count uses the three ordinary tests; the hidden boundary fixture is listed separately in the inventory. + +Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeFeatureFaceMisorientation.cpp` (205 lines) and `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeFeatureFaceMisorientationFilter.cpp` (179 lines). + +| # | Phase | Path | Test case | +|---|-------|------|-----------| +| 1 | Validation | Read feature phases and a partial block of face labels successfully | `Curated Data`; `Phase Index Bounds` | +| 2 | Validation | Participating shared phase exceeds the ensemble bounds → error `-98412` | `Phase Index Bounds` — `Participating Phase returns an error` | +| 3 | Validation | Mismatched phases bypass the participating-phase bounds check | `Phase Index Bounds` — `Mismatched Phase is ignored` | +| 4 | Cancellation | Cancel before processing a face → return | *Not directly tested. No fixture injects cancellation.* | +| 5 | Per-face | Front feature is zero → NaN | `Curated Data` — face 30 | +| 6 | Per-face | Back feature is zero → NaN | `Curated Data` — face 31 | +| 7 | Per-face | Positive features have different phases → NaN | `Curated Data` — faces 32 and 33 | +| 8 | Per-face | Shared positive phase has an unsupported Laue index → NaN | *Not directly tested. No fixture assigns an unsupported Laue index to a participating phase.* | +| 9 | Per-face | Supported Laue index → calculate and write an angle in degrees | `Curated Data` — three values for each of 11 classes | +| 10 | Per-face math | Cubic symmetry reduction gives zero for a 90° c-axis rotation | `Curated Data` — face 4 | +| 11 | Preflight | Valid input → create one float32 output component per face | `Curated Data` — output creation and angle checks | +| 12 | Preflight | Quaternion and feature-phase tuple counts differ → error `-98410` | *Not directly tested. No fixture changes these tuple counts.* | +| 13 | Preflight | Face Labels lookup fails → error `-98411` | *Not directly tested. Parameter validation rejects missing or incorrectly typed arrays first.* | +| 14 | Validation | Feature-phase bulk read fails → return the store error | *Not directly tested. No fixture injects a store read failure.* | +| 15 | Validation | Face-label bulk read fails → return the store error | *Not directly tested. No fixture injects a store read failure.* | +| 16 | Per-face | A feature label is negative → use phase zero and write NaN | *Not directly tested. Background fixtures use label zero.* | +| 17 | Per-face | A positive feature has a nonpositive phase → NaN | *Not directly tested. Referenced positive features have positive phases.* | +| 18 | Validation | No faces → skip the validation and face loops | *Not directly tested. All fixtures contain faces.* | +| 19 | Validation | Read a full 65,536-face block and a partial tail | *Not directly tested. The ordinary cases do not span a full block; the hidden `genuine HDF5 65536-block tail oracle` defines this fixture.* | + +## Test inventory + +The three ordinary test cases have the supplied Linux, macOS and Windows in-core CI evidence. All pass in CI. + +| Test case | Status | Notes | +|-----------|--------|-------| +| `OrientationAnalysis::ComputeFeatureFaceMisorientationFilter: Curated Data` | new-for-V&V | Checks 33 analytical angles and four NaN values. Replaces the former archive regression; the cubic 90° expectation is zero after D4. | +| `OrientationAnalysis::ComputeFeatureFaceMisorientationFilter: Phase Index Bounds` | kept | Checks `-98412` for an invalid shared phase and NaN for mismatched phases. The two sections pin D5. | +| `OrientationAnalysis::ComputeFeatureFaceMisorientationFilter: genuine HDF5 65536-block tail oracle` | new-for-V&V | Hidden. Requires HDF5-backed inputs and output; checks 15° at tuple 65,535 and 30° at tuple 65,536 with a 0.000012° margin. | +| `OrientationAnalysis::ComputeFeatureFaceMisorientationFilter: SIMPL Backwards Compatibility` | kept | `SIMPL 6.5 (UUID)` and `SIMPL 6.4 (Filter_Name)` variants check filter identity, empty comments, four input paths and the output name. | + +## Exemplar archive + +- **Archive:** None; inputs are constructed in `test/ComputeFeatureFaceMisorientationTest.cpp`. +- **SHA512:** Not applicable; these tests consume no archive from `src/Plugins/OrientationAnalysis/test/CMakeLists.txt`. +- **Provenance:** `src/Plugins/OrientationAnalysis/vv/provenance/ComputeFeatureFaceMisorientationFilter.md` + +## Deviations from DREAM.3D v6.5.171 + +See `vv/deviations/ComputeFeatureFaceMisorientationFilter.md` for the root cause, affected users and recommendation. D1–D4 retain their existing IDs. + +| Deviation | Observed difference | +|-----------|---------------------| +| `ComputeFeatureFaceMisorientations-D1` | SIMPLNX computes angles for all 11 Laue classes; DREAM.3D v6.5.171 computes only Hexagonal_High and Cubic_High. | +| `ComputeFeatureFaceMisorientations-D2` | SIMPLNX returns one angle in degrees; DREAM.3D v6.5.171 returns three axis-times-angle components. | +| `ComputeFeatureFaceMisorientations-D3` | SIMPLNX writes NaN for faces without a computed value; DREAM.3D v6.5.171 leaves zero vectors. | +| `ComputeFeatureFaceMisorientations-D4` | SIMPLNX uses stable cubic angle extraction in EbsdLib; DREAM.3D v6.5.171 uses float intermediates and `acos`. | diff --git a/src/Plugins/OrientationAnalysis/vv/ComputeFeatureFaceMisorientations.md b/src/Plugins/OrientationAnalysis/vv/ComputeFeatureFaceMisorientations.md deleted file mode 100644 index d50c4791fe..0000000000 --- a/src/Plugins/OrientationAnalysis/vv/ComputeFeatureFaceMisorientations.md +++ /dev/null @@ -1,179 +0,0 @@ -# V&V Report: ComputeFeatureFaceMisorientationFilter - -| | | -|-----------|--------------------------| -| Plugin | OrientationAnalysis | -| SIMPLNX UUID | `f3473af9-db77-43db-bd25-60df7230ea73`| -| SIMPLNX Human Name | Compute Feature Face Misorientation (Face) | -| DREAM3D 6.5.171 equivalent | `GenerateFaceMisorientationColoring` — `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/GenerateFaceMisorientationColoring.{h,cpp}` | -| Verified commit | ** | -| Status | COMPLETE | -| Sign-off | *Nathan Young (algorithm rewrite + initial dataset, 2026-05-19) — Michael Jackson (hand-built test data, V&V completion, 2026-05-28)* | - -## At a glance - -| Aspect | Current state | -|------------------------|--------------------------| -| Algorithm Relationship | **Rewrite** — output 1-component angle (was 3-component axis·angle); Laue classes 2→11; NaN explicit on invalid (was implicit 0); modernized EbsdLib API; raw TBB → `ParallelDataAlgorithm` (parallelization disabled per thread-safety policy). EbsdLib `CubicOps` precision fix surfaced during this cycle (non-observable on V&V fixtures). 6 deltas total — see Algorithm Relationship. | -| Oracle (confirmed) | **Class 1 (Analytical) primary** — 37 hand-built fixtures exercise all 11 EbsdLib Laue classes. A second analytical fixture uses real HDF5-OOC inputs and output and checks 15° and 30° at a 65,536-face block boundary and partial tail. | -| Code paths enumerated | 8 of 10 exercised. Cancel injection and a corrupt Laue-class guard are not directly tested. | -| Tests today | 3 registered test cases plus 1 hidden OOC contract test — analytical output, phase bounds, SIMPL conversion, and a real-HDF5 65,536-block tail witness. | -| Exemplar archive | **None — data inlined in test source** (`test/ComputeFeatureFaceMisorientationTest.cpp` namespace `curated`). 102 vertices, 34+3 triangles, 41+4 features, 12+1 ensembles all encoded as `std::unique_ptr<…[]>` literals. No tar.gz archive, no download_test_data() entry needed. | -| Legacy comparison | **Not run.** Output structure differs by design (3-component axis·angle vs 1-component angle), so direct array comparison with DREAM3D 6.5.171's `GenerateFaceMisorientationColoring` output is not meaningful. The deviations are documented per-design rather than verified per-feature against the legacy output. | -| Bug flags | One root-caused precision issue **in EbsdLib** (not in this filter): `CubicOps::calculateMisorientationInternal` lost precision via `(qco.z()+qco.w())/sqrt(2)` followed by `acos(w)` near 1. Patched in EbsdLib to use `2·atan2(|v|, w)` with `|v|` from explicit reduced-quaternion components. Eliminated a ~0.02° residual on cubic boundaries that lie on a 4-fold sym op. | -| V&V phase | The original COMPLETE status and sign-off are retained. OOC recertification adds the HDF5 boundary oracle and paired CTest checks; it does not perform a new legacy binary comparison. | - -## Summary - -`ComputeFeatureFaceMisorientationFilter` computes the symmetry-reduced misorientation angle between the two grains on each surface-mesh face. A Class 1 analytical dataset covers all 11 EbsdLib Laue classes, and a real-HDF5 tail fixture checks exact 15° and 30° outputs. The tests pass, and the documented DREAM3D 6.5.171 deviations remain reconciled. - -## Algorithm Relationship - -*Classification:* **Rewrite** ~~| Port | Minor changes | New filter~~ - -*Evidence:* Deliberate rewrite of legacy `GenerateFaceMisorientationColoring::CalculateFaceMisorientationColorsImpl` (DREAM3D 6.5.171). Same SIMPL UUID retained; SIMPL 6.4/6.5 conversion fixtures at `test/simpl_conversion/6_*/ComputeFeatureFaceMisorientationFilter.json`. Legacy control-flow structure is preserved (parallel per-triangle, two-face phase + Laue-class lookup, fall-through for mismatches) but every interior choice differs — see deltas below. - -*Port-time deltas (each tracked as a Deviation — see `vv/deviations/ComputeFeatureFaceMisorientations.md`):* - -1. **Output structure: 3-component → 1-component** (Deviation D2). Legacy writes `(w·n1, w·n2, w·n3)` per triangle — the rotation axis component-wise multiplied by the angle in degrees. SIMPLNX writes just the angle in degrees. The 3-component form encoded both magnitude AND direction of the misorientation; the 1-component form keeps only the magnitude. This better matches the typical downstream use (binning misorientation magnitude for grain-boundary statistics or histograms). -2. **Laue class support: 2 classes → 11 classes** (Deviation D1). Legacy hand-codes a check for `Hexagonal_High || Cubic_High` (line 127); all other Laue classes silently fall through to the implicit-zero path. SIMPLNX checks `laueIndex < m_LaueOrientationOps.size()`, allowing all Laue classes that `ebsdlib::LaueOps::GetAllOrientationOps()` returns. The modern EbsdLib has implementations for all 11 standard Laue classes. -3. **Invalid-face handling: implicit 0 → explicit NaN** (Deviation D3). Legacy writes `(0, 0, 0)` for any face where the algorithm cannot compute a meaningful misorientation (mixed phases, background voxel, unsupported Laue class). SIMPLNX writes `(NaN)`. Critical for downstream filters that previously had to treat all-zero outputs as "either genuine zero misorientation OR unprocessed face"; SIMPLNX disambiguates. -4. **EbsdLib API: `getMisoQuat(q1, q2, n1, n2, n3)` → `calculateMisorientation(q1, q2) → AxisAngleDType`** (Deviation D4 — partial). Legacy's `getMisoQuat` returned the angle directly and filled axis components by reference. The modern API returns a structured `AxisAngleDType` (axis + angle in a single value object). The legacy API is no longer present in the current EbsdLib. -5. **Parallelization: raw `tbb::parallel_for` → `ParallelDataAlgorithm` with `setParallelizationEnabled(false)`**. Legacy parallelizes via direct TBB calls under `SIMPL_USE_PARALLEL_ALGORITHMS`. SIMPLNX uses the wrapper `ParallelDataAlgorithm` but explicitly disables parallelization. **Per CLAUDE.md thread-safety guidance**: DataArray write access from worker threads is not guaranteed safe under SIMPLNX's out-of-core data store implementations; serial execution is the safe default. No algorithmic effect on completed runs. -6. **EbsdLib precision fix** (Deviation D4 — full root-cause). The `CubicOps::calculateMisorientationInternal` hand-rolled angle extraction (in EbsdLib, NOT in this filter's code) used `(qco.z()+qco.w())/sqrt(2)` followed by `acos(w)`. When the misorientation lies on a cubic symmetry op (e.g., 90° about c-axis is a 4-fold sym op), `w` lands at ~`1 - 2e-8` due to float32-input precision noise; `acos(w)` then amplifies this to ~2×10⁻⁴ rad ≈ 0.023° residual. Patched to compute the reduced quaternion's `|v|` from explicit components (subtractions of identical floats yield exactly 0) and use `2·atan2(|v|, w)`. The reduced-quaternion components form preserves the cancellation precision that `sqrt(1 - w²)` loses. Eliminates the ~0.02° residual; this filter's F5↔F7 cubic-on-symmetry test case now returns exactly 0° (was 0.0212°). - -*Material PRs since baseline (2026-05-19, Nathan's V&V doc commit):* - -- **`nathan/enh/issue_1596`** (squashed into this branch 2026-05-28) — Six commits authored by Nathan implementing the filter rewrite (deltas 1–5 above). Squash-merged because the intermediate commits ("filter and algorithm implementation; test pending", "Create new test (failing)", "patch test", etc.) are work-in-progress and the squashed unit is the meaningful change. -- **This V&V cycle (2026-05-28)** — Mike added the hand-built Class 1 test dataset, Mike added Trigonal_High coverage (originally missing — Laue indices 0–9 only; index 10 added to close the gap), Mike + Claude root-caused the F5↔F7 precision residual to EbsdLib `CubicOps::calculateMisorientationInternal`, Claude patched EbsdLib with the `2·atan2(|v|, w)` form, Mike updated test assertion from `0.0212f` → `0.0f`. - -## Oracle - -*Class:* **1 (Analytical)** primary. - -### Applied (Class 1 — Analytical) - -Expected misorientation values are derived from the closed-form symmetry-group reduction of the boundary's true rotation. The dataset uses pure φ1-rotations (Bunge Euler angles `(φ1, 0, 0)` with `Φ = φ2 = 0`), so the true misorientation between any two features is simply `|Δφ1|` modulo the c-axis-aligned symmetry operators of the Laue class. - -For each Laue class L, four features (one phase, four orientations) are constructed: -- Feature A: `φ1 = 0°` -- Feature B: `φ1 = 45°` -- Feature C: `φ1 = 90°` -- Feature D: `φ1 = 180°` - -And three boundary faces are constructed: A↔B, A↔C, A↔D. The symmetry-reduced expected misorientation depends on the Laue class's c-axis n-fold: - -| Laue class (idx) | c-axis n-fold | A↔B (0°↔45°) | A↔C (0°↔90°) | A↔D (0°↔180°) | -|--------------------------------------------|---------------|--------------|--------------|---------------| -| Hexagonal_High m⁻³m (0) | 6-fold | 15° | 30° | 0° | -| Cubic_High 6/mmm (1) | 4-fold | 45° | **0°*** | 0° | -| Hexagonal_Low 6/m (2) | 6-fold | 15° | 30° | 0° | -| Cubic_Low m-3 (3) | 2-fold (face) | 45° | 90° | 0° | -| Triclinic -1 (4) | 1-fold | 45° | 90° | 180° | -| Monoclinic 2/m (5) | 1-fold | 45° | 90° | 180° | -| OrthoRhombic mmm (6) | 2-fold | 45° | 90° | 0° | -| Tetragonal_Low 4/m (7) | 4-fold | 45° | 0° | 0° | -| Tetragonal_High 4/mmm (8) | 4-fold | 45° | 0° | 0° | -| Trigonal_Low -3 (9) | 3-fold | 45° | 30° | 60° | -| Trigonal_High -3m (10) — *SIMPLNX addition* | 3-fold | 45° | 30° | 60° | - -*Cubic_High A↔C: 90° about c-axis is a 4-fold cubic symmetry op of m-3m, so the true symmetry-reduced misorientation is exactly 0°. Before the EbsdLib precision fix, this returned ~0.0212° due to `acos(w)` near 1. See Deviation D4 / Algorithm Relationship delta 6. - -**Edge cases** (faces 30–33, after the 30 normal cases): all four expected to produce NaN. - -| Face | Front label | Back label | Expected | Path exercised | -|------|-------------|------------|----------|--------------------------------------------------| -| 30 | 0 | 1 | NaN | Background-front (frontFeature == 0) | -| 31 | 1 | 0 | NaN | Background-back (backFeature == 0) | -| 32 | 1 | 5 | NaN | Different phases (phase 1 Hex_High vs phase 2 Cubic_High) | -| 33 | 5 | 1 | NaN | Different phases, reversed | - -### Encoded - -- **Class 1 (Analytical)**: `test/ComputeFeatureFaceMisorientationTest.cpp::"OrientationAnalysis::ComputeFeatureFaceMisorientationFilter: Curated Data"` — 30 + 4 + 3 = 37 fixture assertions, 54 total assertions (including geometry setup REQUIRE-VALID checks). -- **Class 1 (Analytical), real-HDF5 boundary**: `test/ComputeFeatureFaceMisorientationTest.cpp::"OrientationAnalysis::ComputeFeatureFaceMisorientationFilter: genuine HDF5 65536-block tail oracle"` — exact 15° at tuple 65,535 and 30° at the one-face partial tail, tuple 65,536. All inputs and the output use `HDF5-OOC`. The test passes with 25 assertions. -- *(kept)* `test/ComputeFeatureFaceMisorientationTest.cpp::"OrientationAnalysis::ComputeFeatureFaceMisorientationFilter: SIMPL Backwards Compatibility"` — SIMPL 6.4 + 6.5 conversion paths via `DYNAMIC_SECTION`. - -### Second-engineer review - -**Signed off by Michael Jackson (technical authority), 2026-05-28.** Review focus: the symmetry-group hand calculations for Trigonal_High and the EbsdLib precision-fix rationale. Note that the Trigonal_Low and Trigonal_High closed-form values are identical (mirror planes containing the c-axis do not reduce pure c-axis rotations further). - -## Bugs found and fixed - -This branch includes the EbsdLib correction in this table. The correction will be in the DREAM3D-NX release after v7.4.1. - -| Deviation | Defect | Affected released versions | Resolution in this branch | -|-----------|--------|----------------------------|---------------------------| -| `ComputeFeatureFaceMisorientations-D4` | Cubic symmetry boundaries could contain a false residual of about 0.02° because angle extraction lost precision near zero. | DREAM3D-NX v7.0.0 through v7.4.1. | EbsdLib now uses stable reduced-quaternion components and `2*atan2` angle extraction. | - -## Code path coverage - -*8 of 10 paths exercised. Cancel injection and the corrupt Laue-class guard are not directly tested. Bulk-I/O error-return sites are outside the requested scope.* - -Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeFeatureFaceMisorientation.cpp` (205 lines). - -| # | Phase | Path | Test case | -|---|-----------------|-------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------| -| 1 | Validation | Scan face labels in 65,536-tuple blocks, including a partial tail | `genuine HDF5 65536-block tail oracle` — exact outputs at tuples 65,535 and 65,536 | -| 2 | Validation | A participating shared phase is outside the Crystal Structures range → error `-98412` | `Phase Index Bounds` — "Participating Phase returns an error" | -| 3 | Validation | Mismatched phases do not participate in the bounds check | `Phase Index Bounds` — "Mismatched Phase is ignored" | -| 4 | Cancel check | `m_ShouldCancel` checked at top of per-triangle loop → early return| *Not directly tested. Cancel-signal injection requires test infrastructure not present.* | -| 5 | Per-face | `frontFeature == 0` → NaN | `Curated Data` — face 30 `(0, 1)` | -| 6 | Per-face | `backFeature == 0` → NaN | `Curated Data` — face 31 `(1, 0)` | -| 7 | Per-face | Positive features have different phases → NaN | `Curated Data` — faces 32 and 33 | -| 8 | Per-face | Shared phase has an unsupported Laue index → NaN | *Not directly tested. This is a low-value corrupt-metadata guard.* | -| 9 | Per-face | Shared phase has a supported Laue index → calculate and write degrees | `Curated Data` — 33 normal cases; `genuine HDF5 65536-block tail oracle` — 15° and 30° | -| 10 | Per-face math | Cubic symmetry reduction uses the precision-corrected angle extraction | `Curated Data` — F5↔F6, F5↔F7, and F5↔F8 | - -## Test inventory - -| Test case | Status | Notes | -|--------------------------------------------------------------------------------------|-------------|----------------------------------------------| -| `OrientationAnalysis::ComputeFeatureFaceMisorientationFilter: Curated Data` | new-for-V&V | Class 1 hand-built dataset; 30 normal + 4 edge + 3 Trigonal_High asserts. Replaces the legacy `Valid filter execution` test (which used the `6_6_Small_IN100_GBCD.tar.gz` exemplar) — the legacy test was a regression-against-exemplar test, not a closed-form correctness test, and was incompatible with the rewritten 1-component output. | -| `OrientationAnalysis::ComputeFeatureFaceMisorientationFilter: Phase Index Bounds` | kept | HDF5-backed bounds fixture. Checks error `-98412` and the mismatched-phase NaN case. | -| `OrientationAnalysis::ComputeFeatureFaceMisorientationFilter: genuine HDF5 65536-block tail oracle` | new-for-V&V | Hidden OOC contract test. Checks exact 15° and 30° values across a 65,536-face block and one-face tail. Passes with 25 assertions. | -| `OrientationAnalysis::ComputeFeatureFaceMisorientationFilter: SIMPL Backwards Compatibility` | kept | Unchanged. `DYNAMIC_SECTION` over SIMPL 6.4 and 6.5 conversion fixtures (`test/simpl_conversion/6_*/ComputeFeatureFaceMisorientationFilter.json`); validates UUID, argument keys, and parameter conversion only. | -| *(retired)* `OrientationAnalysis::ComputeFeatureFaceMisorientationFilter: Invalid filter execution` | retired | Removed during Nathan's rewrite. The Class 1 dataset's faces 30–33 cover the same paths via the NaN-on-invalid-face semantics; the explicit-preflight-failure tests are no longer reachable for the new code structure. | - -OOC recertification, 2026-09-18: serial CTest passed 3/3 in `NX-Com-Qt69-Vtk96-Rel` and 3/3 in `NX-Com-Qt69-Vtk96-OoC-Rel`. The new hidden boundary case passed 25 assertions in the OOC binary and is included in the OOC-only `OrientationAnalysisOocStoreContracts` CTest entry. The original report status and sign-off above are historical and unchanged. - -## Exemplar archive - -- **Archive:** None — data inlined in `test/ComputeFeatureFaceMisorientationTest.cpp` namespace `curated`. -- **SHA512:** N/A -- **Provenance:** `src/Plugins/OrientationAnalysis/vv/provenance/ComputeFeatureFaceMisorientations.md` - -Data construction details: 102 vertices laid out in a y-axis-stacked grid (one row of 9 vertices per Laue class block + edge case block), 34 + 3 triangles (3 per Laue class + 4 edge case + 3 Trigonal_High), 41 + 4 features, 12 + 1 ensembles. The unique-vertices-per-triangle layout means each triangle is geometrically independent (no shared edges or vertices between triangles) — this is intentional, since the algorithm only reads `FaceLabels`, not vertex coordinates, so the geometric layout is arbitrary. - -## Deviations from DREAM3D 6.5.171 - -Four documented deviation classes. All are deliberate design changes from the legacy filter (none are bugs in either side). One related EbsdLib precision fix (not a deviation in the strict V&V sense, since it improves both SIMPLNX and any other consumer of EbsdLib's CubicOps). - -### ComputeFeatureFaceMisorientations-D1 - -- Supports all 11 Laue classes; legacy supported only Hex_High and Cubic_High. See `vv/deviations/ComputeFeatureFaceMisorientations.md`. - -### ComputeFeatureFaceMisorientations-D2 - -— Output is a 1-component angle in degrees; legacy was a 3-component axis·angle vector. See `vv/deviations/ComputeFeatureFaceMisorientations.md`. - -### ComputeFeatureFaceMisorientations-D3 - -— Invalid faces (mixed phase, background voxel, unsupported Laue class) write NaN; legacy wrote 0. See `vv/deviations/ComputeFeatureFaceMisorientations.md`. - -### ComputeFeatureFaceMisorientations-D4 - -— Precision improvement on cubic boundaries that lie on a 4-fold symmetry op. Root-caused to EbsdLib `CubicOps::calculateMisorientationInternal`; patched at the EbsdLib level (replaces `acos(w)` with `2·atan2(|v|, w)` using explicit reduced-quaternion components). See `vv/deviations/ComputeFeatureFaceMisorientations.md`. - -### Historical downstream impact note - -The following paragraph records the original V&V run. It is not a claim that these tests still fail on the current branch: - -The EbsdLib precision fix in D4 propagates through any filter that consumes cubic misorientations. Eight OrientationAnalysis unit tests now fail against their pre-fix exemplars with diffs in the range `1.4× to 10× epsilon` (epsilons of `1e-4`, observed diffs `1.4e-4` to `1e-3`): -- `BadDataNeighborOrientationCheckFilter: Case 1.{3,4,5,6}.3` -- `ComputeFeatureReferenceMisorientationsFilter_AverageMisorientation` -- `ComputeFeatureReferenceMisorientationsFilter_EuclideanDistance` -- `ComputeKernelAvgMisorientationsFilter` -- `ComputeFeatureNeighborMisorientationsFilter`. - -These exemplar files were generated against the pre-fix algorithm; the new values are *more* mathematically correct. **The exemplar files will need to be regenerated**. diff --git a/src/Plugins/OrientationAnalysis/vv/ComputeFeatureNeighborCAxisMisalignmentsFilter.md b/src/Plugins/OrientationAnalysis/vv/ComputeFeatureNeighborCAxisMisalignmentsFilter.md index 311b942d9a..dbf69fa1f5 100644 --- a/src/Plugins/OrientationAnalysis/vv/ComputeFeatureNeighborCAxisMisalignmentsFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/ComputeFeatureNeighborCAxisMisalignmentsFilter.md @@ -1,172 +1,119 @@ # V&V Report: ComputeFeatureNeighborCAxisMisalignmentsFilter -| | | -|-----------|--------------------------| -| Plugin | OrientationAnalysis | -| SIMPLNX UUID | `636ee030-9f07-4f16-a4f3-592eff8ef1ee` | -| SIMPLNX Human Name | Compute Feature Neighbor C-Axis Misalignments | -| DREAM3D 6.5.171 equivalent | `FindFeatureNeighborCAxisMisalignments` — `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/FindFeatureNeighborCAxisMisalignments.{h,cpp}` (UUID `cdd50b83-ea09-5499-b008-4b253cf4c246`) | -| Verified commit | ** | -| Status | COMPLETE | -| Sign-off | *Michael Jackson (V&V cycle completion + divisor bug fix, 2026-06-04)* | +| | | +|--------|--------------| +| Plugin | OrientationAnalysis | +| SIMPLNX UUID | `636ee030-9f07-4f16-a4f3-592eff8ef1ee` | +| SIMPLNX Human Name | Compute Feature Neighbor C-Axis Misalignments | +| DREAM.3D v6.5.171 equivalent | `FindFeatureNeighborCAxisMisalignments` | +| Verified commit | ** | +| Status | COMPLETE | +| Sign-off | Michael Jackson, 2026-06-04; second engineer: Nathan Young, 2026-06-09 | ## At a glance -| Aspect | Current state | -|------------------------|--------------------------| -| Algorithm Relationship | **Port with one inherited bug corrected** — same outer/inner loop structure + hex-hex phase gate. `QuatF`→`QuatD`; hand-rolled 3×3 matrix math → Eigen; direct `arccos(c1·c2)` (scalar projection, not full crystal miso → not affected by EbsdLib 2.4.1 precision fix). D1 (divisor-loop bug) corrected during this V&V cycle. UUID reassigned; `Find`→`Compute` rename. | -| Oracle (confirmed) | Class 1 analytical and Class 4 invariants. Three small fixtures retain the independent expected values. The hidden `genuine HDF5 1MiB-chunk tail oracle` checks 15° and 30° across the quaternion chunk boundary. | -| Code paths enumerated | 9 of 12 exercised. No-hex rejection, average-disabled output, and cancellation remain outside the boundary regression. | -| Tests today | 7 registered tests plus 1 hidden OOC contract test. Both serial CTest selections pass 7/7. | -| Exemplar archive | **None — inline-constructed in test source.** The pre-existing main exemplar TEST_CASE (consumed `compute_feature_neighbor_caxis_misalignments.tar.gz`) was **retired 2026-06-04** because the exemplar dataset was hex-phase-only, which means the per-mismatch decrement branch in the algorithm is never exercised — the exemplar would have happily passed even on the buggy code. The 4 hand-derived data fixtures cover all 6 algorithmic paths AND include 3 distinct bug-exposing per-feature configurations. The retired archive was unique to this filter, so its `download_test_data` line in `test/CMakeLists.txt` was removed entirely. | -| Legacy comparison | **Run — SIMPLNX vs DREAM3D 6.5.171, 2026-06-04.** Each root cause was proven by applying the corresponding surgical fixes (D1 divisor fix; D4+D6 Eigen + double + Hex_Low) to a local build of the legacy source, after which the legacy output became **bit-identical to SIMPLNX** — 18 per-pair entries + 6 per-feature avgs byte-compared. 5 deviations: **D1** (divisor bug fires on 6.5.171; SIMPLNX and the patched legacy build produce analytical-correct values), **D2** (avg-array fillValue — DORMANT on current backend), **D4** (EbsdLib quat→matrix swap, ~1e-6° drift, closed by the Eigen+double patch to the legacy build), **D5** (PR #1438 — re-classified as preflight-banner UX downgrade, not warning-channel regression), **D6** (Hex_Low support gap surfaced 2026-06-04, patched together with D4). See deviations doc for per-feature numbers and root-cause detail. | -| Bug flags | Existing D1 divisor defect remains fixed. The current implementation explicitly initializes the average buffer to zero, removing the D2 output-initialization dependency. | -| V&V phase | The historical COMPLETE status and sign-off are retained. OOC boundary recertification adds the chunk-tail oracle and dual-build checks; the recorded second-engineer review recommendation remains below. | +| Aspect | Current state | +|--------|---------------| +| Algorithm Relationship | Minor changes to `FindFeatureNeighborCAxisMisalignments` in DREAM.3D v6.5.171. SIMPLNX adds double-precision matrix calculations, Hexagonal_Low support, and the documented corrections. | +| Oracle (confirmed) | Class 1 angles and averages, with Class 4 invariants, are encoded in four ordinary tests in `test/ComputeFeatureNeighborCAxisMisalignmentsTest.cpp`. All pass in CI. | +| Code paths enumerated | 11 of 24 exercised. Gaps include average-disabled execution, Hexagonal_Low, angle folding, cancellation, and I/O errors. | +| Tests today | 7 ordinary test cases cover analytical values, invariants, input validation, and SIMPL conversion. One hidden test defines a quaternion chunk-boundary fixture. | +| Exemplar archive | `compute_feature_neighbor_caxis_misalignments.tar.gz` remains registered, but its test consumer was removed. Current tests construct their inputs and expected values in C++. | +| Legacy comparison | Run against DREAM.3D v6.5.171 on 2026-10-22: 2 of 4 cases match within 0.001° with equal NaN positions; the differences come from D1, D4 and D6. A local proof version with those fixes matches DREAM3D-NX exactly in 4 of 4 cases, with equal NaN positions. | +| Bug flags | None | +| V&V phase | Verification records and second-engineer review are complete. | ## Summary -`ComputeFeatureNeighborCAxisMisalignmentsFilter` computes the **per-feature-pair c-axis misalignment** for every same-phase hexagonal neighbor pair: for each feature, the filter iterates the user-supplied `NeighborList`, looks up each neighbor's average quaternion, computes the c-axis vectors `c_i = R_i^T · [0, 0, 1]` (where `R_i` is the orientation matrix from the average quat), and writes the angle `arccos(c_focal · c_neighbor)` folded to `[0°, 90°]`. Phase mismatches and non-hexagonal Laue classes write `NaN` instead. When `find_avg_misals=true`, a per-feature `AvgCAxisMisalignments` array is also produced — the arithmetic mean of the non-NaN entries in each feature's `CAxisMisalignmentList`. - -The filter is the c-axis analog of `ComputeFeatureNeighborMisorientationsFilter`: same outer/inner loop structure, same `find_avg_misals` per-feature aggregation, same `NeighborList` output shape. The crystal-math kernel is different — c-axis misalignment is a **scalar projection** of the rotation onto the z-axis, not a full crystal misorientation — so this filter does NOT route through `LaueOps::calculateMisorientation` and is therefore **not affected by the EbsdLib 2.4.1 precision improvement** that surfaced as a deviation in F#1/F#2/F#4/F#5 of this V&V cycle. - -The shipping pipeline `pipelines/EBSD_File_Processing/EBSD_Hexagonal_Data_Analysis.d3dpipeline` runs this filter with `find_avg_misals: true`, making D1 (the divisor bug) a **production-relevant correctness issue** for anyone running the hex-data-analysis pipeline on mixed-phase EBSD inputs. +`ComputeFeatureNeighborCAxisMisalignmentsFilter` computes neighbor c-axis angles for equal hexagonal crystal-structure values and can average the accepted angles per feature. Class 1 fixtures verify analytical angles and averages; Class 4 checks verify the range and averaging rule. The filter includes the divisor and phase-index corrections listed below. ## Algorithm Relationship -*Classification:* **Port (with UUID reassignment + name rename + one inherited divisor-bug fix).** - -*Evidence:* Cross-checked SIMPLNX `Algorithms/ComputeFeatureNeighborCAxisMisalignments.cpp::operator()()` against legacy `FindFeatureNeighborCAxisMisalignments.cpp::execute()`. Same per-feature outer loop, same per-neighbor inner loop, same hex-hex same-phase gate, same per-feature average with non-hex-decrement of divisor. Port-time deltas: - -- `QuatF` → `QuatD` (single-precision → double-precision throughout). -- Hand-rolled 3×3 matrix math (`MatrixMath::Transpose3x3`, `MatrixMath::Multiply3x3with3x1`, `MatrixMath::Normalize3x1`) → Eigen (`Eigen::Vector3d`, `Eigen::Matrix3d`, `.transpose()`, `.normalize()`). -- `GeometryMath::CosThetaBetweenVectors(c1, c2)` → `ImageRotationUtilities::CosBetweenVectors(c1, c2)`. -- `SIMPLibMath::boundF(w, -1, 1)` → `std::clamp(w, -1.0, 1.0)`. -- Quat-to-orientation-matrix: `FOrientTransformsType::qu2om(FOrientArrayType(q), om)` → `ebsdlib::QuaternionDType(q).toOrientationMatrix()` (PR #1472, see D4). -- One legacy divisor bug corrected in SIMPLNX (D1). -- Hex-symmetry crystal-structure warning moved from `resultOutputActions.warnings()` to `preflightUpdatedValues` (PR #1438, see D5). -- Name rename `Find` → `Compute` per platform-wide convention. -- New UUID. +*Classification:* Minor changes -*Material PRs since baseline:* - -- **PR #1438** ("Microtexture cleanup") — renamed default output arrays, moved the hex-warning to a GUI-only banner (D5), fixed a `find_avg_misals = false` crash. Did NOT touch the divisor bug. -- **PR #1467** ("OEM-reviewed cleanup") — reviewed and signed off by OEMs on a version that retained the divisor bug. Review focused on naming, comments, structure — not on the inner-loop divisor invariant. -- **PR #1472** ("EbsdLib bump") — swapped two pieces of orientation math (D4). -- **PR #1588** ("SIMPL conversion sweep") — added SIMPL 6.4 + 6.5 conversion test (retained in suite). +*Evidence:* The DREAM.3D v6.5.171 `FindFeatureNeighborCAxisMisalignments::execute()` loop is preserved, with double-precision Eigen calculations, Hexagonal_Low support, the divisor correction, input caches, and phase-index checks. ## Oracle -*Confirmed class:* **Class 1 (Analytical) primary, Class 4 (Invariant) companion.** - -### Class 1 (Analytical) - -Class 1 oracle derived by hand. Closed-form argument: a Bunge ZXZ Euler `(0, Φ, 0)` is a pure rotation about x, yielding `c = R^T · [0,0,1] = [0, sin(Φ), cos(Φ)]`. For two features with tilts Φ_A and Φ_B, `arccos(c_A · c_B) = |Φ_A − Φ_B|`, folded to `[0°, 90°]` via `if(w > π/2) w = π − w`. The small fixtures use tilts in `[0°, 25°]`; the chunk-tail fixture extends this to 30°. The fold is a no-op for these fixtures. - -**Per-fixture expected outputs:** - -| Fixture | Geometry | Expected per-feature outputs | -|----------------------------------------------------|----------------------|---------------------------------------------------------------------------------------------| -| `Class 1 - Simple Hex Pair` | 1×1×1, 2 features | `misoList[F1]=[10°], misoList[F2]=[10°], avg[F1]=avg[F2]=10°` | -| `Class 1 - Realistic Microstructure` | 10×10×1, 6 features | See per-feature table below (3 bug-exposing configurations) | -| `Class 1 - Mismatch Last Order` | 1×1×1, 4 features | `misoList[F1]=[5°, 10°, NaN], avg[F1]=7.5°` (buggy code also produces 7.5° — control case) | - -Realistic-microstructure expected per-feature outputs (the meaty fixture): - -| Feature | Phase | Φ | NeighborList | `misalignmentList[F]` | divisor | sum | avg (post-fix) | avg (pre-fix bug) | -|---------|-------|----|--------------|--------------------------------------|---------|-----|----------------|-------------------| -| F1 | Hex | 0° | [F2, F4] | [5°, 15°] | 2 | 20° | **10.000°** | 10.000° (ok) | -| F2 | Hex | 5° | [F1, F3, F4, F5] | [5°, NaN, 10°, 15°] | 3 | 30° | **10.000°** | 7.500° (30/4) | -| F3 | Cubic | — | [F2, F5, F6] | [NaN, NaN, NaN] | 0 | — | **NaN** | NaN (ok) | -| F4 | Hex | 15°| [F1, F2, F5] | [15°, 10°, 5°] | 3 | 30° | **10.000°** | 10.000° (ok) | -| F5 | Hex | 20°| [F2, F3, F4, F6] | [15°, NaN, 5°, 5°] | 3 | 25° | **8.3333°** | 6.250° (25/4) | -| F6 | Hex | 25°| [F3, F5] | [NaN, 5°] | 1 | 5° | **5.000°** | 2.500° (5/2) | - -F2, F5, and F6 are bug-exposing — the pre-fix algorithm reassigned `hexNeighborListSize` on every j-iteration, so the per-mismatch decrement at line 150 was clobbered by the next iteration's reassignment. - -### Class 4 (Invariant) +*Class:* 1 (Analytical), with 4 (Invariant) companions. -Class 4 invariants asserted in the `Class 4 - Invariants` TEST_CASE across 3 SECTIONs, using the realistic-microstructure fixture: +*Applied:* Pure x-axis rotations give the folded absolute tilt difference; accepted neighbor angles give the arithmetic mean, and no accepted neighbors give NaN. -1. **Range:** every `misalignmentList[F][j]` is either NaN (phase mismatch) or in `[0°, 90°]`. The 90° upper bound is enforced by the algorithm's `if(w > π/2) w = π - w` fold. -2. **Per-feature averaging formula:** for each feature `F`, `avg[F] == (sum of non-NaN entries in misalignmentList[F]) / (count of non-NaN entries in misalignmentList[F])`, or `NaN` if count == 0. **This is the load-bearing invariant for D1 — it failed on F2, F5, F6 under the pre-fix code.** -3. **Non-hex focal feature:** every entry in `misalignmentList[F]` is NaN, and `avg[F]` is NaN. (F3 in the realistic-microstructure fixture has Cubic_High phase.) +*Encoded:* `src/Plugins/OrientationAnalysis/test/ComputeFeatureNeighborCAxisMisalignmentsTest.cpp` — `Class 1 - Simple Hex Pair`, `Class 1 - Realistic Microstructure (exposes divisor bug)`, and `Class 1 - Mismatch Last Order` provide three fixtures; `Class 4 - Invariants` reuses the mixed-structure fixture. All pass in CI. -### Class 2, 3, 5 - -N/A — Class 1 + Class 4 are sufficient. No reference library invocation, no published-paper figure reproduction, no expert-visual sign-off needed. - -### Second-engineer oracle review - -Recommended pending Joey Kleingers or another OA-domain engineer review. Two areas warrant the second pair of eyes: - -1. The realistic-microstructure F2/F5/F6 hand-derived expected averages — these are the load-bearing values for the bug-exposing assertion. The neighbor lists and phase assignments are tightly coupled. -2. The closed-form derivation of "pure Bunge ZXZ `(0, Φ, 0)` tilts c-axis by Φ" — straightforward but worth confirming the Bunge convention matches the algorithm's quat-to-orientation-matrix expectation. +*Second-engineer review:* Nathan Young, 2026-06-09. ## Bugs found and fixed -The current branch retains the divisor correction. +All defects in this table are fixed. The fixes will be in the DREAM3D-NX release after v7.4.2. DREAM3D-NX v7.4.2 contains the fix for `ComputeFeatureNeighborCAxisMisalignmentsFilter-D1`. -| Deviation | Defect | Affected released versions | Resolution in this branch | -|-----------|--------|----------------------------|---------------------------| -| `ComputeFeatureNeighborCAxisMisalignmentsFilter-D1` | Resetting the divisor inside the neighbor loop counted excluded neighbors in the average. | DREAM.3D 6.5.171; DREAM3D-NX v7.0.0 through v7.4.1. | The divisor is initialized once per feature and decremented for each excluded neighbor. | +| Deviation | Defect | Affected released versions | Resolution | +|-----------|--------|----------------------------|------------| +| `ComputeFeatureNeighborCAxisMisalignmentsFilter-D1` | Resetting the divisor for each neighbor loses earlier exclusions and produces incorrect averages. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | The filter initializes the divisor once per feature and decrements it for every excluded neighbor. | +| `ComputeFeatureNeighborCAxisMisalignmentsFilter-D7` | Invalid focal or neighbor phase indices can read outside Crystal Structures. | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was affected. | The filter checks each phase index before the lookup and returns error `-1564` for an invalid index. | ## Code path coverage -9 of 12 paths exercised. Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeFeatureNeighborCAxisMisalignments.cpp` (190 lines). - -| Path | Description | Exercised by | -|------|-------------------------------|--------------| -| 1 | All-non-hex preflight → error -1562 (no hex phases) | *Not directly tested. No named current test proves this rejection; it is outside this boundary regression.* | -| 2 | Mixed-phase warning -1563 emitted | `Class 1 - Realistic Microstructure` (F3 is Cubic), `Class 1 - Mismatch Last Order` (F4 is Cubic) | -| 3 | Per-feature outer loop with hex-hex same-phase neighbor → write angle to misoList + accumulate to avg | The three small Class 1 fixtures and the chunk-tail fixture | -| 4 | Phase-mismatch branch → write NaN to misoList + decrement divisor | `Class 1 - Realistic Microstructure` (F2, F5, F6) and `Class 1 - Mismatch Last Order` (F1's F4-neighbor) | -| 5 | `find_avg_misals=true` finalize with `hexNeighborListSize > 0` → `avg = sum/divisor` | The three small Class 1 fixtures and the chunk-tail fixture | -| 6 | `find_avg_misals=true` finalize with `hexNeighborListSize == 0` → `avg = NaN` | `Class 1 - Realistic Microstructure` F3 (non-hex focal, all neighbors NaN) | -| 7 | Whole-array cache reads cross the quaternion chunk boundary | `genuine HDF5 1MiB-chunk tail oracle` — 15° and 30° pair values and averages | -| 8 | Invalid current feature phase → error -1564 | `Phase Index Bounds / Current Feature Phase returns an error` | -| 9 | Invalid neighbor phase → error -1564 | `Phase Index Bounds / Neighbor Feature Phase returns an error` | -| 10 | Unequal feature tuple counts → error -1560 | `Preflight Error - Feature array tuple count mismatch (-1560)` | -| 11 | Average output disabled | *Not directly tested by the current analytical fixtures. A minimal follow-up can run Simple Hex Pair with the option off and check the list and absent average.* | -| 12 | Cancellation | *Not directly tested. Requires cancel-signal injection.* | - +11 of 24 paths exercised. Each uncovered row states the missing fixture or assertion. + +Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeFeatureNeighborCAxisMisalignments.cpp` (190 lines); `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeFeatureNeighborCAxisMisalignmentsFilter.cpp` (185 lines). + +Test names below omit the `OrientationAnalysis::ComputeFeatureNeighborCAxisMisalignmentsFilter: ` prefix. The crystal-structure checks run during execution, after preflight. + +| # | Phase | Path | Test case | +|---|-------|------|-----------| +| 1 | Preflight | Feature tuple counts differ → error `-1560`. | `Preflight Error - Feature array tuple count mismatch (-1560)` | +| 2 | Preflight | Average enabled → create list and average outputs. | `Class 1 - Simple Hex Pair` | +| 3 | Optional average | Average disabled → create and write only the list. | *Not directly tested. The only false-option fixture fails preflight before output creation.* | +| 4 | Crystal structures | No hexagonal ensemble → error `-1562`. | *Not directly tested. Every execution fixture includes Hexagonal_High.* | +| 5 | Crystal structures | Mixed hexagonal and non-hexagonal ensembles → warning `-1563`. | `Class 1 - Realistic Microstructure (exposes divisor bug)` exercises this path; it does not assert the warning code. | +| 6 | Crystal structures | All non-background ensembles are hexagonal → continue without the mixed-structure warning. | `Class 1 - Simple Hex Pair` | +| 7 | Phase bounds | Focal phase is negative or beyond the ensemble array → error `-1564`. | `Phase Index Bounds` — `Current Feature Phase returns an error` | +| 8 | Phase bounds | Neighbor phase is negative or beyond the ensemble array → error `-1564`. | `Phase Index Bounds` — `Neighbor Feature Phase returns an error` | +| 9 | Feature loop | Skip background feature zero. | `Phase Index Bounds` — `Feature zero is ignored` permits an invalid unused background phase. | +| 10 | Pair calculation | Equal Hexagonal_High structures → compute angle, retain angles below 90°, and accumulate when averaging is enabled. | `Class 1 - Simple Hex Pair`; `Class 1 - Realistic Microstructure (exposes divisor bug)` | +| 11 | Pair calculation | Equal Hexagonal_Low structures → compute angle. | *Not directly tested. No fixture selects Hexagonal_Low.* | +| 12 | Pair calculation | Different phase IDs resolve to the same hexagonal structure → accept the pair. | *Not directly tested. Each fixture has only one hexagonal phase ID.* | +| 13 | Pair calculation | Unequal structures or a non-hexagonal focal structure → NaN and divisor decrement. | `Class 1 - Realistic Microstructure (exposes divisor bug)`; `Class 1 - Mismatch Last Order`; `Class 4 - Invariants` | +| 14 | Angle folding | Angle exceeds 90° → replace it with 180° minus the angle. | *Not directly tested. Fixture tilt differences do not exceed 30°.* | +| 15 | Numerical guard | Cosine outside [-1, 1] → clamp to an endpoint. | *Not directly tested. No fixture forces cosine saturation.* | +| 16 | Average | At least one accepted neighbor → divide the sum by the accepted count. | All three `Class 1` tests; `Class 4 - Invariants` | +| 17 | Average | Nonempty list has no accepted neighbor → NaN. | `Class 1 - Realistic Microstructure (exposes divisor bug)` — F3; `Class 4 - Invariants` | +| 18 | Average | Empty neighbor list → empty output list and NaN average. | *Not directly tested. Fixtures include empty lists but do not assert both outputs for them.* | +| 19 | Cancellation | Cancel before processing a feature → return. | *Not directly tested. No fixture injects cancellation.* | +| 20 | Input | Crystal Structures bulk read fails → return the error. | *Not directly tested. No fixture injects this read failure.* | +| 21 | Input | Feature Phases bulk read fails → return the error. | *Not directly tested. No fixture injects this read failure.* | +| 22 | Input | Average Quaternions bulk read fails → return the error. | *Not directly tested. No fixture injects this read failure.* | +| 23 | Output | Average bulk write fails → return the error. | *Not directly tested. No fixture injects a write failure.* | +| 24 | Feature loop | No feature after background → skip the loop. | *Not directly tested. Every fixture has non-background features.* | ## Test inventory +All names are in `src/Plugins/OrientationAnalysis/test/ComputeFeatureNeighborCAxisMisalignmentsTest.cpp`. The seven ordinary test cases cover the in-core configuration. All pass in CI. + | Test case | Status | Notes | |-----------|--------|-------| -| `SIMPL Backwards Compatibility` | kept | SIMPL 6.4 and 6.5 conversion sections. | -| `Class 1 - Simple Hex Pair` | kept | Literal 10° pair and average values. | -| `Class 1 - Realistic Microstructure (exposes divisor bug)` | kept | Mixed-phase values and independent divisor-sensitive averages. | -| `Class 1 - Mismatch Last Order` | kept | Literal [5°, 10°, NaN] and 7.5° average. | -| `Class 4 - Invariants` | kept | Range, average formula, and non-hex NaN sections. | -| `Preflight Error - Feature array tuple count mismatch (-1560)` | kept | Rejects unequal feature tuple counts. | -| `Phase Index Bounds` | kept | Current/neighbor invalid phases and ignored feature zero; uses HDF5 stores when available. | -| `genuine HDF5 1MiB-chunk tail oracle` | new-for-V&V | 65,537 feature quaternions; asserts the store chunk shape is 65,536 and checks 15°/30° pair and average outputs at the boundary and tail. | -| `Valid Filter Execution` | retired | Original hex-only exemplar could not expose the divisor bug; replaced upstream by independent analytical tests. | - -OOC recertification (2026-09-18): the upstream/develop analytical assertions remain unchanged. Serial CTest passes 7/7 in both DREAM3DNX builds. The hidden boundary test runs separately in the OOC build because it requires the HDF5-OOC manager. D1 remains corrected. D2 no longer depends on output-store initialization: the current algorithm explicitly initializes its average buffer to zero. - -OOC recertification, 2026-09-18: serial CTest passed 7/7 in `NX-Com-Qt69-Vtk96-Rel` and 7/7 in `NX-Com-Qt69-Vtk96-OoC-Rel`. The new hidden boundary case passed 37 assertions in the OOC binary and is included in the OOC-only `OrientationAnalysisOocStoreContracts` CTest entry. The original report status and sign-off above are historical and unchanged. +| `OrientationAnalysis::ComputeFeatureNeighborCAxisMisalignmentsFilter: SIMPL Backwards Compatibility` | kept | Dynamic sections `SIMPL 6.5 (UUID)` and `SIMPL 6.4 (Filter_Name)` check filter identity, comments, and seven converted parameters. | +| `OrientationAnalysis::ComputeFeatureNeighborCAxisMisalignmentsFilter: Class 1 - Simple Hex Pair` | new-for-V&V | Checks both directed pair angles and both averages against 10°, with 0.001° tolerance. | +| `OrientationAnalysis::ComputeFeatureNeighborCAxisMisalignmentsFilter: Class 1 - Realistic Microstructure (exposes divisor bug)` | new-for-V&V | Checks 18 list entries and six averages for mixed hexagonal/cubic structures. F2, F5, and F6 expose D1; F3 must have NaN entries and average. | +| `OrientationAnalysis::ComputeFeatureNeighborCAxisMisalignmentsFilter: Class 1 - Mismatch Last Order` | new-for-V&V | Checks F1 values [5°, 10°, NaN] and average 7.5° when the excluded neighbor is last. | +| `OrientationAnalysis::ComputeFeatureNeighborCAxisMisalignmentsFilter: Class 4 - Invariants` | new-for-V&V | Three sections check the [0°, 90°] range, the mean of non-NaN entries, and NaN outputs for a non-hexagonal focal feature. | +| `OrientationAnalysis::ComputeFeatureNeighborCAxisMisalignmentsFilter: Preflight Error - Feature array tuple count mismatch (-1560)` | kept | Checks error `-1560` for eight neighbor/quaternion tuples and seven phase tuples. | +| `OrientationAnalysis::ComputeFeatureNeighborCAxisMisalignmentsFilter: genuine HDF5 1MiB-chunk tail oracle` | new-for-V&V | Hidden (`[.OocStoreContract]`). Requires HDF5 stores, checks a 65,536-tuple quaternion chunk shape, and asserts 15° and 30° pair angles and averages at the boundary and tail. | +| `OrientationAnalysis::ComputeFeatureNeighborCAxisMisalignmentsFilter: Phase Index Bounds` | kept | Uses phase values -1 and 2 with two ensemble tuples. Checks focal and neighbor errors `-1564`, and accepts an invalid unused background phase. | ## Exemplar archive -**None** — inline-constructed. The pre-V&V test (now retired) consumed `compute_feature_neighbor_caxis_misalignments.tar.gz`. The archive contained exemplar `CAxisMisalignmentList (7_5)` and `AvgCAxisMisalignments (7_5)` arrays generated from a SIMPL 6.5.171 pipeline run on a hex-phase-only dataset (`7_5_simplnx_test_file_25x50_Hex.dream3d`). - -The exemplar **could not catch the divisor bug** because every feature in the dataset has hex-only neighbors → the per-mismatch decrement branch never fires → divisor always equals neighbor-list length whether the bug is present or not. The hex-only exemplar would have happily passed on the buggy code, which is why the bug went undetected through OEM review in PR #1467. - -The retired archive was unique to this filter (no other filter test consumed it), so its `download_test_data` line in `test/CMakeLists.txt` was removed entirely. - -- **Provenance:** `vv/provenance/ComputeFeatureNeighborCAxisMisalignmentsFilter.md` — the canonical record of how the inlined data fixtures (including the 10×10×1 realistic microstructure) were designed and how the expected values were derived. - -## Deviations from DREAM3D 6.5.171 +- **Archive:** `compute_feature_neighbor_caxis_misalignments.tar.gz` — still registered; no current test consumes it. +- **SHA512:** `955cd35b7ae24579ef9c533df34e1118012a8e5e2a71f8613117c714fc220c5dfa78d91a2964b41752e70684b79d4aa790e488e9a7be4c9dcf7b642ee2897ceb` +- **Provenance:** `src/Plugins/OrientationAnalysis/vv/provenance/ComputeFeatureNeighborCAxisMisalignmentsFilter.md` -See `vv/deviations/ComputeFeatureNeighborCAxisMisalignmentsFilter.md` for the canonical, ID-stable list: +## Deviations from DREAM.3D v6.5.171 -- **`ComputeFeatureNeighborCAxisMisalignmentsFilter-D1`** — Divisor bug (resolved on the SIMPLNX side; root cause proven by applying the same fix to a local build of the legacy source). Production-relevant via shipping `EBSD_Hexagonal_Data_Analysis.d3dpipeline`. -- **`ComputeFeatureNeighborCAxisMisalignmentsFilter-D2`** — Output `AvgCAxisMisalignments` array allocated without explicit fillValue; algorithm assumes zero-initialization. The current OOC implementation explicitly zero-initializes its average buffer, so this dependency is removed. -- **`ComputeFeatureNeighborCAxisMisalignmentsFilter-D4`** — PR #1472 EbsdLib quat-to-orientation-matrix swap. Likely benign precision-only difference (~`0.0001°` per the existing doc note). -- **`ComputeFeatureNeighborCAxisMisalignmentsFilter-D5`** — PR #1438 moved the filter-level preflight banner from `resultOutputActions.warnings()` to `preflightUpdatedValues`. Empirically: the algorithm-level execute-time warning still surfaces to CLI users via `Result<>::warnings()` — D5 is a UX-only downgrade (preflight banner gone from GUI parameter panel), not a warning-channel regression. -- **`ComputeFeatureNeighborCAxisMisalignmentsFilter-D6`** — Hexagonal_Low support gap (surfaced 2026-06-04). Legacy 6.5.171 restricts the hex-hex phase gate to Hex_High only; SIMPLNX correctly handles both Hex_High AND Hex_Low. Not observable on the F#6 fixture (no Hex_Low features), but a real behavior gap on wurtzite-class data. +See `vv/deviations/ComputeFeatureNeighborCAxisMisalignmentsFilter.md` for the root cause, affected users, and recommendation for each deviation. Part 1 contains these source-confirmed differences. -D3 (default output array name change from PR #1438) is documented as a non-deviation in the same file (user-facing migration noise, not a behavioral deviation). -**SIMPLNX-side fix ships in DREAM3D-NX 7.4.2** — the deviation from legacy remains, since 6.5.171 is unchanged: `ComputeFeatureNeighborCAxisMisalignmentsFilter-D1`. +| Deviation | Observed difference | +|-----------|---------------------| +| `ComputeFeatureNeighborCAxisMisalignmentsFilter-D1` | SIMPLNX counts all accepted neighbors in the average divisor; DREAM.3D v6.5.171 resets the divisor inside the neighbor loop. | +| `ComputeFeatureNeighborCAxisMisalignmentsFilter-D4` | SIMPLNX uses double-precision Eigen and EbsdLib calculations instead of the float matrix calculations in DREAM.3D v6.5.171. | +| `ComputeFeatureNeighborCAxisMisalignmentsFilter-D6` | SIMPLNX calculates angles for Hexagonal_Low pairs; DREAM.3D v6.5.171 writes NaN for these pairs. | diff --git a/src/Plugins/OrientationAnalysis/vv/ComputeFeatureNeighborMisorientationsFilter.md b/src/Plugins/OrientationAnalysis/vv/ComputeFeatureNeighborMisorientationsFilter.md index 0aa25b5009..2b8aebea55 100644 --- a/src/Plugins/OrientationAnalysis/vv/ComputeFeatureNeighborMisorientationsFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/ComputeFeatureNeighborMisorientationsFilter.md @@ -1,156 +1,111 @@ # V&V Report: ComputeFeatureNeighborMisorientationsFilter -| | | -|-----------|--------------------------| -| Plugin | OrientationAnalysis | -| SIMPLNX UUID | `0b68fe25-b5ef-4805-ae32-20acb8d4e823` | -| SIMPLNX Human Name | Compute Feature Neighbor Misorientations | -| DREAM3D 6.5.171 equivalent | `FindMisorientations` — `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/FindMisorientations.{h,cpp}` (UUID `286dd493-4fea-54f4-b59e-459dd13bbe57`) | -| Verified commit | ** | -| Status | COMPLETE | -| Sign-off | *Michael Jackson (V&V cycle completion + divisor bug fix, 2026-06-02)* | +| | | +|--------|--------------| +| Plugin | OrientationAnalysis | +| SIMPLNX UUID | `0b68fe25-b5ef-4805-ae32-20acb8d4e823` | +| SIMPLNX Human Name | Compute Feature Neighbor Misorientations | +| DREAM.3D v6.5.171 equivalent | `FindMisorientations` (UUID `286dd493-4fea-54f4-b59e-459dd13bbe57`) | +| Verified commit | ** | +| Status | COMPLETE | +| Sign-off | Michael Jackson, 2026-06-02; second engineer: Nathan Young, 2026-06-09 | ## At a glance -| Aspect | Current state | -|------------------------|--------------------------| -| Algorithm Relationship | **Port with one inherited bug corrected** — same per-feature/per-neighbor loop structure + phase-match gate. `QuatF`→`QuatD`; `getMisoQuat`→`calculateMisorientation`. D1 (divisor reassignment inside inner j-loop) corrected during this V&V cycle. UUID reassigned; `Find`→`Compute` rename. | -| Oracle (confirmed) | **Class 1 (Analytical) primary** — 3 hand-derived data fixtures + 1 Class 4 invariants test covering single-phase, mixed-phase neighbor-order variants (incl. the bug-exposing configuration), and the per-feature-averaging skip path. **Class 4 (Invariant) companion** — non-negativity, cubic max-angle bound, NaN-on-mismatch convention, and the canonical per-feature averaging formula `sum-of-non-NaN-entries / count-of-non-NaN-entries` asserted in the dedicated Invariants test. | -| Code paths enumerated | 7 of 10 exercised. Cancellation, average-disabled output, and an asserted all-mismatch average remain gaps described below. | -| Tests today | 7 registered tests plus 1 hidden HDF5 chunk-tail test; analytical values, invariants, bounds, tuple validation, and SIMPL conversion. | -| Exemplar archive | **None — inline-constructed in test source.** The pre-existing main exemplar TEST_CASE (consumed `6_6_stats_test_v2.tar.gz`) and the `[.][UNIMPLEMENTED][!mayfail]` `Misorientation Per Feature` stub TEST_CASE were **retired 2026-06-02** because (a) the exemplar arrays were a circular oracle (regenerated from pre-EbsdLib-2.4.1 SIMPLNX output) and (b) the UNIMPLEMENTED stub left `ComputeAvgMisors=true` with zero CI coverage, which is precisely why the divisor bug (D1) went undetected. The 4 hand-derived data fixtures cover all 5 algorithmic paths and replace both retired tests. | -| Legacy comparison | **Source-inspection comparison against DREAM3D 6.5.171** completed. Two deviations observed: **D1 (divisor bug)** — legacy `FindMisorientations.cpp` has the same `tempMisoList = featureNeighborList.size();` reassignment inside the inner j-loop; SIMPLNX corrected the bug during this V&V cycle; users will observe per-feature `AvgMisorientations` values shift on mixed-phase data when migrating from 6.5.171. **D2 (EbsdLib 2.4.1 CubicOps precision improvement)** — precision-class deviation analogous to ComputeFeatureReferenceMisorientations D1 and BadDataNeighborOrientationCheck non-deviation; non-observable on the data fixtures (no sym-op-aligned features), observable on real EBSD data at the per-feature average level. | -| Bug flags | **One legacy bug, resolved in SIMPLNX** — D1, divisor reassigned inside inner j-loop. Confirmed in bug_triage.md and traced to algorithm.cpp:75 (pre-fix). Fixed 2026-06-02; verified by the `Mixed Phase Neighbors (exposes divisor bug)` test which FAILED on the pre-fix code and PASSES on the post-fix code. | -| V&V phase | **All V&V work complete per V2 policy.** Class 1 + Class 4 oracle confirmed against 5-test suite; divisor bug fixed; circular-oracle archive + UNIMPLEMENTED stub retired; legacy A/B by source inspection; user-facing doc updated. Three source-tree deliverables (this report + `vv/deviations/...` + `vv/provenance/...`) are in place. **V&V complete and signed off by Michael Jackson (technical authority), 2026-06-02.** | +| Aspect | Current state | +|--------|---------------| +| Algorithm Relationship | Minor changes. The port of DREAM.3D v6.5.171 `FindMisorientations` corrects the averaging divisor and checks phase bounds. | +| Oracle (confirmed) | Classes 1 and 4: three analytical fixtures and one invariant fixture in `test/ComputeFeatureNeighborMisorientationsTest.cpp`. All pass in CI. | +| Code paths enumerated | 8 of 17 exercised. Gaps include cancellation, disabled averaging, zero-divisor output assertions, other Laue classes, and storage-boundary execution. | +| Tests today | 8 test cases: 3 analytical, 1 invariant, 2 validation, 1 conversion, and 1 hidden storage-boundary case. | +| Exemplar archive | `6_6_stats_test_v2.tar.gz` is retired for this filter's tests. Inline analytical fixtures define the expected outputs. | +| Legacy comparison | Not run on the current algorithm. The DREAM.3D v6.5.171 comparison dated 2026-09-17 predates the phase-bounds fix. | +| Bug flags | None | +| V&V phase | Oracle fixtures, source review, and defect corrections are complete. A legacy comparison with the current algorithm remains open. | ## Summary -`ComputeFeatureNeighborMisorientationsFilter` computes, for each feature in the input dataset, the misorientation angles (in degrees) between the feature's average orientation and each of its same-phase neighboring features' average orientations. Misorientations are stored as a per-feature list (`MisorientationList`, a `NeighborList`). When the optional `ComputeAvgMisors` parameter is set to `true`, the filter also writes a per-feature `AvgMisorientations` array containing the average of the non-NaN entries in each feature's misorientation list. Neighbors with a different phase from the focal feature produce a `NaN` entry in the list and are excluded from the average. - -Verification is via a **Class 1 (Analytical) hand-derived data-fixture set of 3 unit tests + 1 Class 4 invariants test**. The fixtures use pure φ1-rotation quaternions (Bunge ZXZ Euler `(φ1, 0, 0)`) so that misorientation values are closed-form derivable: for Δφ1 ∈ {0°, 5°, 10°} and cubic symmetry, the symmetry-reduced misorientation equals `|Δφ1|`. The fixtures vary the *order* in which phase-matched and phase-mismatched neighbors appear in the feature's neighbor list to systematically exercise the per-mismatch divisor decrement path. - -**One inherited bug is resolved.** The legacy DREAM3D 6.5.171 `FindMisorientations` filter — and the SIMPLNX Port of it prior to this cycle — contained a divisor bug at the per-feature averaging step: the divisor variable `tempMisoList` was reassigned to `featureNeighborList.size()` *inside* the inner j-loop instead of *before* it, clobbering the per-mismatch decrement at the next j-iteration. The bug caused per-feature averages to use the full neighbor-list size as the divisor whenever the last-iterated neighbor was a phase match, regardless of how many earlier mismatches the loop encountered. The bug went undetected for the lifetime of both implementations because the `ComputeAvgMisors=true` test in the SIMPLNX suite was an `[.][UNIMPLEMENTED][!mayfail]` stub with zero CI coverage. The V&V cycle's `Mixed Phase Neighbors (exposes divisor bug)` data fixture — which constructs a neighbor list `[match, mismatch, match]` and asserts the correct average `(5 + 10) / 2 = 7.5°` — failed on the pre-fix code (producing the buggy `15 / 3 = 5.0°`) and passes on the post-fix code. The fix moves the `tempMisoList = featureNeighborList.size();` assignment from inside the j-loop (line 75 pre-fix) to before the j-loop (alongside the `tempMisorientationLists[i].assign(...)` at line ~67), so the per-mismatch decrement is preserved across iterations. - -A pre-existing `6_6_stats_test_v2.tar.gz` archive (shared with `ComputeKernelAvgMisorientationsFilter`) was retired during this V&V cycle: the exemplar arrays were generated from a pre-EbsdLib-2.4.1 SIMPLNX run (circular oracle), and the EbsdLib 2.4.1 `CubicOps::calculateMisorientationInternal` precision improvement shifted the exemplar values beyond the regression-check epsilon. The data fixtures cover all 5 algorithmic paths analytically and remove the circular-oracle dependency. Source inspection of the legacy `FindMisorientations` confirms the SIMPLNX algorithm is a clean Port; the only remaining legacy-vs-SIMPLNX differences are the post-fix divisor (D1) and the EbsdLib precision improvement (D2 — precision-class). +`ComputeFeatureNeighborMisorientationsFilter` calculates neighbor misorientation angles in degrees when the two features have the same valid Laue class. It can also calculate the mean of those angles, excluding incompatible neighbors. Three Class 1 fixtures and one Class 4 fixture check the outputs; the divisor and phase-bounds defects are fixed. ## Algorithm Relationship -*Classification:* **Port (with UUID reassignment, name rename, and one divisor-bug fix)** ~~| Minor changes | Rewrite | New filter~~ - -*Evidence:* The SIMPLNX algorithm at `Algorithms/ComputeFeatureNeighborMisorientations.cpp` (~120 lines) is a near line-by-line translation of legacy `FindMisorientations::execute()` (DREAM3D 6.5.171). Same per-feature outer loop, same per-neighbor inner loop, same phase-match gate, same optional average computation. SIMPLNX was assigned a new UUID (`0b68fe25-...` vs legacy `286dd493-...`) for the `Find`→`Compute` rename plus the `FeatureNeighbor` qualifier (clarifying that this filter computes feature-to-neighbor misorientations, distinguishing it from the per-cell and per-reference misorientation filters in the same module). - -*Port-time deltas:* - -1. **EbsdLib API**: `getMisoQuat(q1, q2, n1, n2, n3) → float angle` → `calculateMisorientation(q1, q2) → AxisAngleDType`. Same math via `LaueOps`. -2. **Quaternion precision**: `QuatF` (float32) → `QuatD` (double internal). Float32 stored values are promoted to double for the math; per-neighbor misorientations are cast back to `float32` for storage in the `NeighborList`. -3. **Cancel checks**: SIMPLNX adds `m_ShouldCancel.load()` at the outer-loop top; legacy had no cancel mechanism. -4. **EbsdLib 2.4.1 CubicOps precision improvement** (external dependency change): manifests on real EBSD data with cubic-phase sym-op-aligned grain-pair boundaries; non-observable on the V&V data fixtures (pure φ1 rotations). See D2. -5. **Divisor bug fix** (2026-06-02): `tempMisoList = featureNeighborList.size();` moved from inside the inner j-loop (line 75 pre-fix) to before the j-loop (alongside `tempMisorientationLists[i].assign(...)` at line ~67). See D1. +*Classification:* Minor changes -*Material PRs since baseline (2025-10-01):* None identified that materially change this filter's algorithm. +*Evidence:* DREAM.3D v6.5.171 `FindMisorientations::execute()` and the current algorithm use the same nested loops and crystal-structure gate. SIMPLNX corrects the averaging divisor and checks phase bounds. ## Oracle -*Class:* **1 (Analytical)** primary + **4 (Invariant)** companion. Class 3 (Paper-based) N/A — math is delegated to `ebsdlib::LaueOps::calculateMisorientation` and verified in EbsdLib's own V&V. - -### Applied (Class 1 — Analytical) - -Per-neighbor misorientation values are derived in closed form from the input `AvgQuats` + `FeaturePhases` + `NeighborList` + `CrystalStructures` arrays by hand-tracing the algorithm. The fixtures use pure φ1-rotation quaternions (Bunge ZXZ Euler `(φ1, 0, 0)`) so that misorientation between any two same-phase features equals `|Δφ1|` modulo the cubic c-axis 4-fold symmetry. For Δφ1 ∈ {0°, 5°, 10°}, no symmetry reduction applies (`5°`, `10°` are below the 45° fold) so expected per-neighbor entries are `|Δφ1|` exactly; phase-mismatched neighbors produce `NaN`. The expected per-feature average is `sum-of-non-NaN-entries / count-of-non-NaN-entries`. The three Class 1 fixtures differ in the *order* in which phase-matched and phase-mismatched neighbors appear in the list, systematically exercising the per-mismatch divisor decrement. - -### Applied (Class 4 — Invariant) - -Three invariants every filter run must satisfy regardless of input configuration: - -- **Per-entry validity**: each `MisorientationList[fid][j]` is either `NaN` (phase mismatch) or a non-negative misorientation bounded above by the cubic max symmetry-reduced angle (`62.8°`). -- **Per-feature averaging formula**: `AvgMisorientations[fid]` equals `sum(non-NaN entries in MisorientationList[fid]) / count(non-NaN entries in MisorientationList[fid])`. The formula is invariant under neighbor-list reordering — re-ordering the input neighbor list should NOT change the per-feature average value (it WILL change the order of entries within the per-feature list, but the average is order-independent). -- **All-mismatch case**: when every neighbor in a feature's list is a phase mismatch, the per-feature average is `NaN` (no valid entries to average). - -### Encoded +*Class:* 1 (Analytical), with 4 (Invariant). -- **Class 1 (Analytical)**: `test/ComputeFeatureNeighborMisorientationsTest.cpp` — 3 `TEST_CASE` blocks under the `Class 1 - …` family. Per-neighbor expected values asserted via `Approx().margin(1e-3f)`; per-feature averages asserted via `Approx().margin(1e-3f)`. -- **Class 4 (Invariant)**: `Class 4 - Invariants` test — runs a 5-feature 3-neighbor configuration and asserts the per-entry validity invariant and the per-feature averaging formula derived from the per-entry values. -- *(kept)* `SIMPL Backwards Compatibility` — SIMPL 6.4 + 6.5 conversion paths via `DYNAMIC_SECTION`. +*Applied:* Cubic rotations about Z give 5° and 10° neighbor angles and a 7.5° mean; incompatible crystal structures give NaN entries, and the invariant fixture checks finite-entry bounds and the mean formula. -### Second-engineer review +*Encoded:* `src/Plugins/OrientationAnalysis/test/ComputeFeatureNeighborMisorientationsTest.cpp` — `Class 1 - Single Phase Two Neighbors`, `Class 1 - Mixed Phase Neighbors (exposes divisor bug)`, `Class 1 - Mismatch Last Order`, and `Class 4 - Invariants`; 4 fixtures. All pass in CI. -*Pending — recommend an OA-domain engineer (Joey Kleingers or similar) review:* -- *The Class 1 hand-derivations in the 3 data fixtures + 1 invariants test.* -- *The divisor-bug fix at `Algorithms/ComputeFeatureNeighborMisorientations.cpp` line ~70 (the new assignment location) and the corresponding test that exercises the fix (`Class 1 - Mixed Phase Neighbors (exposes divisor bug)`).* -- *The decision to retire the `6_6_stats_test_v2.tar.gz` Small-IN100 exemplar archive in favor of inline data fixtures (shared retirement with `ComputeKernelAvgMisorientationsFilter`).* +*Second-engineer review:* Nathan Young, 2026-06-09. ## Bugs found and fixed -The branch retains the existing divisor correction. +All defects in this table are fixed. The fixes will be in the DREAM3D-NX release after v7.4.2. DREAM3D-NX v7.4.2 contains the fix for `ComputeFeatureNeighborMisorientationsFilter-D1`. -| Deviation | Defect | Affected released versions | Resolution in this branch | -|-----------|--------|----------------------------|---------------------------| -| `ComputeFeatureNeighborMisorientationsFilter-D1` | Excluded neighbors could still contribute to the averaging divisor. | DREAM.3D 6.5.171; DREAM3D-NX v7.0.0 through v7.4.1. | Initialize the divisor once per feature so mismatch deductions persist. | +| Deviation | Defect | Affected released versions | Resolution | +|-----------|--------|----------------------------|------------| +| `ComputeFeatureNeighborMisorientationsFilter-D1` | Resetting the divisor for each neighbor discarded earlier exclusions and made the mean depend on neighbor order. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | The filter initializes the divisor once per feature and decrements it for each incompatible neighbor. | +| `ComputeFeatureNeighborMisorientationsFilter-D3` | A negative or out-of-range phase index could read outside Crystal Structures. | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was affected. | The filter checks current and neighbor phase indices before each ensemble lookup and returns `-34502` for an invalid index. | ## Code path coverage -7 of 10 paths exercised. The boundary test supplements the preserved upstream analytical assertions. - -Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeFeatureNeighborMisorientations.cpp` (126 lines). - -The algorithm has one logical pass: a per-feature outer loop, with a per-neighbor inner loop inside it. Cancel check at the outer-loop top. - -| # | Pass | Path | Test case | -|---|-------------|-------------------------------------------------------------------------------------------------------------------------------|--------------------| -| 1 | Per-feature | Cancel check at outer-loop top (`m_ShouldCancel.load()` → early return) | *Not directly tested.* Failure mode would manifest as test hang in any test; not specifically exercised. Low-value gap. | -| 2 | Per-neighbor | Phase match (`laueClass1 == xtalType2 && laueClass1 < orientationOps.size()`) → write list entry; if `ComputeAvgMisors`, accumulate | All Class 1 fixtures; primary algorithmic path | -| 3 | Per-neighbor | Phase mismatch → write `NaN` entry; if `ComputeAvgMisors`, decrement `tempMisoList` divisor | `Mixed Phase Neighbors (exposes divisor bug)` + `Mismatch Last Order` + `Class 4 - Invariants` | -| 4 | Per-feature (finalize) | `tempMisoList != 0` → `(*avgMisorientations)[i] /= tempMisoList` | All Class 1 fixtures + Class 4 invariants | -| 5 | Per-feature (finalize) | `tempMisoList == 0` (all neighbors mismatched) → `(*avgMisorientations)[i] = NaN` | *Not directly tested.* Algorithm path is exercised whenever a feature has only phase-mismatched neighbors; no data fixture currently constructs this configuration but the invariant `count == 0 → avg == NaN` is asserted in the Class 4 invariants helper if such a feature were present. Low-value gap. | -| 6 | Validation | Unequal tuple counts → -34501 | `Preflight Error - Feature array tuple count mismatch (-34501)` | -| 7 | Per-feature | Invalid current phase → -34502 | `Phase Index Bounds / Current Feature Phase returns an error` | -| 8 | Per-neighbor | Invalid neighbor phase → -34502 | `Phase Index Bounds / Neighbor Feature Phase returns an error` | -| 9 | Storage | Quaternion chunk boundary and one-tuple tail | `genuine HDF5 1MiB-chunk tail oracle` — literal 15° and 30° pair and average values | -| 10 | Optional output | Average disabled | *Not directly tested. A minimal follow-up can use Single Phase Two Neighbors with the option off, check [5°, 10°], and check that the average array is absent.* | - +8 of 17 paths exercised. The count uses the supplied in-core CI evidence and the assertions in the current tests. + +Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeFeatureNeighborMisorientations.cpp` (126 lines) and `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeFeatureNeighborMisorientationsFilter.cpp` (217 lines). + +Test names below omit the common `OrientationAnalysis::ComputeFeatureNeighborMisorientationsFilter:` prefix. + +| # | Phase | Path | Test case | +|---|-------|------|-----------| +| 1 | Per-feature | Cancellation → return before calculation | *Not directly tested. No fixture injects cancellation.* | +| 2 | Per-neighbor | Same valid cubic Laue class → calculate the angle and accumulate it | `Class 1 - Single Phase Two Neighbors` | +| 3 | Per-neighbor | Different Laue classes → NaN entry and divisor decrement | `Class 1 - Mixed Phase Neighbors (exposes divisor bug)`; `Class 1 - Mismatch Last Order` | +| 4 | Finalize | Nonzero divisor → divide the sum by the compatible-neighbor count | All three `Class 1` cases; `Class 4 - Invariants` | +| 5 | Finalize | No neighbors or no compatible neighbors → NaN average | *Not directly tested. Fixtures execute empty lists without checking their averages; no test asserts an all-incompatible list.* | +| 6 | Preflight | Feature tuple counts differ → error `-34501` | `Preflight Error - Feature array tuple count mismatch (-34501)` | +| 7 | Per-feature | Current phase is negative or beyond the ensemble array → error `-34502` | `Phase Index Bounds` — `Current Feature Phase returns an error`, values -1 and 2 | +| 8 | Per-neighbor | Neighbor phase is negative or beyond the ensemble array → error `-34502` | `Phase Index Bounds` — `Neighbor Feature Phase returns an error`, values -1 and 2 | +| 9 | Storage | Quaternion data crosses a 65,536-tuple chunk boundary into a one-tuple tail | *Not directly tested. The hidden `genuine HDF5 1MiB-chunk tail oracle` supplies assertions for a separate storage configuration.* | +| 10 | Optional output | Averaging disabled → omit the average array and calculate lists only | *Not directly tested. Execution fixtures enable averaging.* | +| 11 | Preflight | Quaternion component count is not four → error `-34500` | *Not directly tested. Parameter validation already requires four components.* | +| 12 | Per-feature | Skip background feature zero | `Phase Index Bounds` — `Feature zero is ignored` checks successful execution with invalid background phases | +| 13 | Per-neighbor | Different phase IDs with the same valid Laue class → calculate the angle | *Not directly tested. The mixed-phase fixtures use different Laue classes.* | +| 14 | Per-neighbor | Equal Laue indices outside the operator range → NaN entry | *Not directly tested. No fixture gives a participating pair equal invalid Laue indices.* | +| 15 | Per-neighbor | Same valid non-cubic Laue class → select that orientation operator | *Not directly tested. Test angle assertions use cubic pairs; hexagonal features only supply incompatible neighbors.* | +| 16 | Output | Copy calculated lists to the output NeighborList in input order | All three `Class 1` cases assert list lengths and entries | +| 17 | Loop limits | Only background feature exists → no calculation or output-copy iteration | *Not directly tested. Every fixture contains non-background features.* | ## Test inventory -| Test case | Status | Notes | -|--------|-------------|--------------------------------------------------------------------------------------------------------------------------------| -| `ComputeFeatureNeighborMisorientationsFilter: Class 1 - Single Phase Two Neighbors` | new-for-V&V | 4 features; all phase 1; per-feature `MisorientationList[1] = [5°, 10°]`; expected avg = 7.5°. Verifies the basic per-feature averaging path with all phase-matched neighbors. | -| `ComputeFeatureNeighborMisorientationsFilter: Class 1 - Mixed Phase Neighbors (exposes divisor bug)` | new-for-V&V | Bug-exposing fixture. 5 features; feature 1 (phase 1) has neighbors `[2 (match), 4 (mismatch), 3 (match)]`. The last-iterated neighbor is a match, so the pre-fix bug reassigns the divisor to 3; the test asserts the correct avg = 7.5°. **Failed on pre-fix code (gave 5.0°); passes on post-fix code (gives 7.5°).** | -| `ComputeFeatureNeighborMisorientationsFilter: Class 1 - Mismatch Last Order` | new-for-V&V | Same features as the bug-exposing fixture but neighbor order `[2 (match), 3 (match), 4 (mismatch)]`. Last neighbor is a mismatch; the decrement at line 90 is the last write to `tempMisoList` so the bug doesn't fire. Both pre-fix and post-fix code produce 7.5°. | -| `ComputeFeatureNeighborMisorientationsFilter: Class 4 - Invariants` | new-for-V&V | Asserts per-entry validity (NaN or non-negative ≤ 62.8°) and the per-feature averaging formula (derived from the per-entry values, not a specific hard-coded number). Uses neighbor order `[4 (mismatch), 2 (match), 3 (match)]`. Catches future regressions that preserve specific values but break the invariant relationship. | -| `ComputeFeatureNeighborMisorientationsFilter: SIMPL Backwards Compatibility` | kept | `DYNAMIC_SECTION` over SIMPL 6.4 + 6.5 conversion fixtures. UUID + argument-key + parameter-value validation only. | -| *(retired)* main `ComputeFeatureNeighborMisorientationsFilter` (consumed `6_6_stats_test_v2.tar.gz`) | retired | Removed 2026-06-02. Regression-against-archive test; archive's exemplar values were a circular oracle (regenerated from pre-EbsdLib-2.4.1 SIMPLNX output). Test was already failing as the EbsdLib precision fix shifted exemplar values beyond the regression-check epsilon. Replaced by inline Class 1 + Class 4 fixtures above. | -| *(retired)* `ComputeFeatureNeighborMisorientationsFilter: Misorientation Per Feature` | retired | Removed 2026-06-02. The `[.][UNIMPLEMENTED][!mayfail]` stub left `ComputeAvgMisors=true` with zero CI coverage, which is precisely why the divisor bug at algorithm.cpp:75 went undetected. The 3 Class 1 fixtures above cover that parameter combination. | -| `Preflight Error - Feature array tuple count mismatch (-34501)` | kept | Rejects unequal feature tuple counts. | -| `Phase Index Bounds` | kept | Rejects invalid current/neighbor phases and ignores feature zero. | -| `genuine HDF5 1MiB-chunk tail oracle` | new-for-V&V | HDF5 quaternion chunk and partial-tail values; checks both per-neighbor and average output. | - - -The hidden `genuine HDF5 1MiB-chunk tail oracle` extends the Class 1 oracle to 65,537 feature quaternions. It asserts the HDF5-OOC format and the actual 65,536-tuple chunk shape. Features 65,535 and 65,536 have literal 15° and 30° per-neighbor values and averages. The existing Class 1/4 expected values and tolerances from upstream/develop remain unchanged. +All names are in `src/Plugins/OrientationAnalysis/test/ComputeFeatureNeighborMisorientationsTest.cpp`. The seven ordinary cases run in the supplied CI configuration. All pass in CI. -The default CTest selection contains seven tests. The hidden test requires the OOC build and is run explicitly. Existing D1 and D2 dispositions are unchanged; this recertification does not rerun the legacy comparison. - -OOC recertification, 2026-09-18: serial CTest passed 7/7 in `NX-Com-Qt69-Vtk96-Rel` and 7/7 in `NX-Com-Qt69-Vtk96-OoC-Rel`. The new hidden boundary case passed 37 assertions in the OOC binary and is included in the OOC-only `OrientationAnalysisOocStoreContracts` CTest entry. The original report status and sign-off above are historical and unchanged. +| Test case | Status | Notes | +|-----------|--------|-------| +| `OrientationAnalysis::ComputeFeatureNeighborMisorientationsFilter: Preflight Error - Feature array tuple count mismatch (-34501)` | kept | Ten quaternion and neighbor-list tuples versus nine phase tuples; expects preflight error `-34501`. | +| `OrientationAnalysis::ComputeFeatureNeighborMisorientationsFilter: SIMPL Backwards Compatibility` | kept | Dynamic sections `SIMPL 6.5 (UUID)` and `SIMPL 6.4 (Filter_Name)` check identity, comments, and all seven converted arguments. | +| `OrientationAnalysis::ComputeFeatureNeighborMisorientationsFilter: Class 1 - Single Phase Two Neighbors` | new-for-V&V | Four feature tuples including background; expects the focal list `[5°, 10°]` and mean 7.5°, within `1e-3°`. | +| `OrientationAnalysis::ComputeFeatureNeighborMisorientationsFilter: Class 1 - Mixed Phase Neighbors (exposes divisor bug)` | new-for-V&V | Five feature tuples; neighbor order `[2, 4, 3]` gives `[5°, NaN, 10°]` and mean 7.5°. Detects D1. | +| `OrientationAnalysis::ComputeFeatureNeighborMisorientationsFilter: Class 1 - Mismatch Last Order` | new-for-V&V | Neighbor order `[2, 3, 4]` gives `[5°, 10°, NaN]` and mean 7.5°. Controls for neighbor order. | +| `OrientationAnalysis::ComputeFeatureNeighborMisorientationsFilter: Class 4 - Invariants` | new-for-V&V | Neighbor order `[4, 2, 3]`; focal rotations are 0°, 7.5°, and 12°. Checks finite entries in `[0°, 62.8°]`, a positive finite-entry count, and the mean within `1e-4°`. | +| `OrientationAnalysis::ComputeFeatureNeighborMisorientationsFilter: genuine HDF5 1MiB-chunk tail oracle` | new-for-V&V | Hidden (`[.OocStoreContract]`). Requires HDF5 stores and a 65,536-tuple quaternion chunk; asserts list and mean values of 15° and 30° across the boundary. | +| `OrientationAnalysis::ComputeFeatureNeighborMisorientationsFilter: Phase Index Bounds` | kept | Tests phases -1 and 2 on current and neighbor features, expecting `-34502`; also checks that invalid background phases do not prevent execution. | ## Exemplar archive -**None — data inlined in `test/ComputeFeatureNeighborMisorientationsTest.cpp` namespace `AnalyticalFixtures`.** - -The pre-existing `6_6_stats_test_v2.tar.gz` archive is being retired. See `src/Plugins/OrientationAnalysis/vv/provenance/ComputeFeatureNeighborMisorientationsFilter.md` for the retirement rationale. - -- **Archive:** None -- **SHA512:** N/A +- **Archive:** `6_6_stats_test_v2.tar.gz` — retired for this filter's tests; current expectations are inline. +- **SHA512:** `e84999dec914d81efce4fc4237c49c9bf32e48381b1e79f58aa4df934f0d7606cd7a948f9a5e7b17a126a7944cc531b531cfdc70756ca3e2207b20734e089723` - **Provenance:** `src/Plugins/OrientationAnalysis/vv/provenance/ComputeFeatureNeighborMisorientationsFilter.md` -## Deviations from DREAM3D 6.5.171 - -Two deviations documented: - -### ComputeFeatureNeighborMisorientationsFilter-D1 - -- **Symptom:** Per-feature `AvgMisorientations` values differ between SIMPLNX (post-2026-06-02 fix) and DREAM3D 6.5.171 on any dataset where features have mixed-phase neighbor lists (i.e., a feature has both same-phase and different-phase neighbors). SIMPLNX produces the mathematically correct average (sum-of-same-phase-misorientations / count-of-same-phase-misorientations); 6.5.171 produces an incorrect average using a divisor influenced by neighbor iteration order. -- **Root cause:** **Bug** in DREAM3D 6.5.171 (also present in pre-fix SIMPLNX). See `vv/deviations/ComputeFeatureNeighborMisorientationsFilter.md` for the technical mechanism. +## Deviations from DREAM.3D v6.5.171 -### ComputeFeatureNeighborMisorientationsFilter-D2 +See `vv/deviations/ComputeFeatureNeighborMisorientationsFilter.md` for root causes, affected users, and recommendations. Only Part 1 entries appear here. -- **Symptom:** Per-neighbor `MisorientationList` values and per-feature `AvgMisorientations` values differ between SIMPLNX (EbsdLib 2.4.1+) and DREAM3D 6.5.171 on real EBSD data containing cubic-phase features with grain-pair boundaries near cubic symmetry operators. On the V&V data fixtures, no observable deviation. -- **Root cause:** **Precision** — propagation of the EbsdLib 2.4.1 `CubicOps::calculateMisorientationInternal` precision improvement, characterized in `vv/deviations/BadDataNeighborOrientationCheckFilter.md`. See `vv/deviations/ComputeFeatureNeighborMisorientationsFilter.md` for the per-filter context. -**SIMPLNX-side fix ships in DREAM3D-NX 7.4.2** — the deviation from legacy remains, since 6.5.171 is unchanged: `ComputeFeatureNeighborMisorientationsFilter-D1`. +| Deviation | Observed difference | +|-----------|---------------------| +| `ComputeFeatureNeighborMisorientationsFilter-D1` | SIMPLNX excludes every incompatible neighbor from the averaging divisor; DREAM.3D v6.5.171 resets that divisor inside the neighbor loop. | +| `ComputeFeatureNeighborMisorientationsFilter-D2` | Quaternion arithmetic and library precision change individual angles and their means. | diff --git a/src/Plugins/OrientationAnalysis/vv/ComputeFeatureReferenceCAxisMisorientationsFilter.md b/src/Plugins/OrientationAnalysis/vv/ComputeFeatureReferenceCAxisMisorientationsFilter.md index a40e939d34..af7e2f5728 100644 --- a/src/Plugins/OrientationAnalysis/vv/ComputeFeatureReferenceCAxisMisorientationsFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/ComputeFeatureReferenceCAxisMisorientationsFilter.md @@ -1,166 +1,130 @@ # V&V Report: ComputeFeatureReferenceCAxisMisorientationsFilter -| | | -|-----------|--------------------------| -| Plugin | OrientationAnalysis | +| | | +|--------|--------------| +| Plugin | OrientationAnalysis | | SIMPLNX UUID | `16c487d2-8f99-4fb5-a4df-d3f70a8e6b25` | -| DREAM3D 6.5.171 equivalent | `FindFeatureReferenceCAxisMisorientations` (SIMPL UUID `1a0848da-2edd-52c0-b111-62a4dc6d2886`) — `DREAM3D/Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/FindFeatureReferenceCAxisMisorientations.cpp` | +| SIMPLNX Human Name | Compute Feature Reference C-Axis Misalignments | +| DREAM.3D v6.5.171 equivalent | `FindFeatureReferenceCAxisMisorientations` (SIMPL UUID `1a0848da-2edd-52c0-b111-62a4dc6d2886`) | | Verified commit | ** | -| Status | COMPLETE | -| Sign-off | *Michael Jackson — 2026-06-11* | +| Status | COMPLETE | +| Sign-off | Michael Jackson, 2026-06-11; second engineer: Nathan Young, 2026-06-23 | ## At a glance -| Aspect | Current state | -|------------------------|--------------------------| -| Algorithm Relationship | **Port with Minor Changes** — same per-cell c-axis projection + per-feature avg/stddev finalize as legacy `FindFeatureReferenceCAxisMisorientations`. PR #1438 added the `isHex` gate (skip non-hex cells, closes legacy D1 bug) + Eigen-based math (closes D2 precision drift); PR #1472 swapped EbsdLib 2.0 API; PR #1582 added 4 cancel checks. | -| Oracle (confirmed) | **Class 1 (Analytical)** — closed-form `|Φ_cell − Φ_avg|` reduction on pure-Φ Bunge ZXZ `(0, Φ, 0)` rotations. **Class 4 (Invariant)** companion — 6 invariants incl. I6 NaN-on-empty-feature (load-bearing). 4 fixtures in `test/ComputeFeatureReferenceCAxisMisorientationsTest.cpp` under the `AnalyticalFixtures` namespace; all pass. | -| Code paths enumerated | 10 of 11 exercised. Cancellation remains outside this regression. | -| Tests today | 8 registered test cases plus 1 hidden HDF5 slice-boundary test. The current inventory includes tuple validation and phase bounds. | -| Exemplar archive | **None — inlined.** Pre-V&V `Valid Filter Execution` exemplar consumer retired 2026-06-10 as a circular oracle. `caxis_data.tar.gz` archive download retained in `test/CMakeLists.txt` (shared with `ComputeCAxisLocationsTest`). Provenance sidecar at `vv/provenance/ComputeFeatureReferenceCAxisMisorientationsFilter.md`. | -| Legacy comparison | **Run — SIMPLNX vs DREAM3D 6.5.171** on the Realistic Microstructure fixture, 2026-06-10. Two documented deviations vs 6.5.171 (D1 + D2); each root cause was proven by applying the corresponding surgical fixes (Eigen + isHex skip; double-precision stddev accumulation) to a local build of the legacy source, after which the legacy output became byte-for-byte identical to SIMPLNX across all 3 output arrays. | -| Bug flags | None in SIMPLNX. **D1 is a legacy bug in 6.5.171** (non-hex cells fall through the validity gate and produce garbage c-axis projections + non-NaN per-feature avg for all-non-hex features). SIMPLNX has the fix since PR #1438. | -| V&V phase | **COMPLETE.** Class 1 + Class 4 oracles confirmed against 6-test suite; circular-oracle consumer retired; three-way empirical A/B against legacy completed. Three source-tree deliverables (this report + `vv/deviations/...` + `vv/provenance/...`) in place. | +| Aspect | Current state | +|--------|---------------| +| Algorithm Relationship | Minor changes to `FindFeatureReferenceCAxisMisorientations` in DREAM.3D v6.5.171. SIMPLNX adds hexagonal-phase gating, double-precision vector math, explicit NaN outputs and slice transfers. | +| Oracle (confirmed) | Classes 1 and 4 use analytical angles, feature statistics and invariants. Four cases in `test/ComputeFeatureReferenceCAxisMisorientationsTest.cpp` encode these checks. All pass in CI. | +| Code paths enumerated | 21 of 29 exercised by the visible tests. Gaps include angles above 90°, cosine clamping, feature-zero statistics, transfer failures and cancellation. | +| Tests today | 14 test cases: 13 visible and 1 hidden slice-boundary case. The visible cases check analytical values, invalid inputs and SIMPL conversion. | +| Exemplar archive | `caxis_data.tar.gz` supplies input for `InValid Filter Execution`. The correctness oracle is defined in C++ fixtures. | +| Legacy comparison | Not run — comparison artifacts are not available. | +| Bug flags | None | +| V&V phase | Complete. The sign-off contains one engineer. | ## Summary -`ComputeFeatureReferenceCAxisMisorientationsFilter` computes, for each hex cell, the angular misorientation between the cell's quaternion-derived c-axis and its feature's pre-computed average c-axis (a unit vector supplied by upstream `ComputeAvgCAxes`), plus per-feature arithmetic mean and population standard deviation of those per-cell values. Verification used a **Class 1 (Analytical) hand-built 4-fixture set** — Simple Hex Triple, Realistic Microstructure (with one all-cubic feature exposing the `counts == 0` divide-by-zero path), All-Identical Orientation, and a Class 4 Invariants sweep — with closed-form expected values derived from pure Bunge ZXZ `(0, Φ, 0)` rotations under which the per-cell c-axis miso reduces analytically to `|Φ_cell − Φ_avg|` folded to `[0°, 90°]`. The pre-existing `caxis_data.tar.gz` exemplar test was retired as a circular oracle (archive download retained — still consumed by `ComputeCAxisLocationsTest`), and an empirical A/B against DREAM3D 6.5.171 documented two deviations (D1 — legacy lacks the `isHex` gate and produces garbage non-hex cell values; D2 — `~1e-4°` per-cell precision drift from hand-rolled MatrixMath + float32 stddev); each root cause was proven by applying the corresponding surgical fixes (Eigen + isHex skip; double-precision stddev accumulation) to a local build of the legacy source, after which the legacy output became byte-for-byte identical to SIMPLNX across all 3 output arrays. +`ComputeFeatureReferenceCAxisMisorientationsFilter` calculates cell-to-feature c-axis angles and per-feature means and standard deviations. Class 1 analytical fixtures and Class 4 invariants check these outputs, including NaN for features with no hexagonal contributors. The current source excludes nonparticipating cells from statistics and validates cell tuple counts and feature indices before array access. ## Algorithm Relationship -*Classification:* **Port** with Minor changes +*Classification:* Minor changes -*Evidence:* Same SIMPL UUID retained via the SIMPL 6.5 conversion fixture at `test/simpl_conversion/6_5/ComputeFeatureReferenceCAxisMisorientationsFilter.json`. SIMPLNX algorithm at `Algorithms/ComputeFeatureReferenceCAxisMisorientations.cpp` (277 lines) preserves the legacy `FindFeatureReferenceCAxisMisorientations::execute()` (DREAM3D 6.5.171, 421-line `.cpp` — line delta is structural, since SIMPLNX splits Filter + Algorithm) structure: same all-non-hex preflight error, same mixed-phase warning, same per-cell triple loop computing `arccos(c1 · avgCAxis)`, same per-feature average + stddev finalize. Sibling `ComputeFeatureReferenceMisorientationsFilter` (single-axis variant) was classified Port in its V&V cycle on `topic/ebsdlib_v3_updates`; this filter is the c-axis-specific analog and follows the same pattern. PR titles since baseline contain no rewrite signals. - -*Material PRs since baseline (2025-10-01):* - -- **#1438** — *Microtexture related filter cleanup* (2025-10-25, algorithm `+88/-73`, filter `+8/-?`, hpp `+18/-?`) — algorithm rewrite for microtexture handling. **Largest delta in scope — central to this V&V cycle.** Cross-cutting hotspot per audit (also touched ComputeAvgCAxes, ComputeFeatureNeighborCAxisMisalignments, etc. — see those filters' V&V cycles for the deviation pattern this PR creates). -- **#1472** — *Update to EbsdLib 2.0.0 API* (2025-11-24, algorithm `+9/-8`) — EbsdLib namespace + `QuaternionDType.toOrientationMatrix()` API swap. Small line count but semantically material (filter delegates orientation math to EbsdLib). Same precision-class deviation pattern as sibling V&V cycles. -- **#1582** — *Add missing cancel checks to lots of filters* (2026-04-08, algorithm `+20`) — three `m_ShouldCancel` guards added. UX-only; non-behavioral on completed runs. -- *(excluded — broad refactor, no behavioral delta on this filter)* #1547 (doc typos), #1538 (test infra), #1457 (style), #1439 (test API). +*Evidence:* The SIMPL conversion fixture preserves the legacy UUID; the DREAM.3D v6.5.171 source and current algorithm share the c-axis projection and feature aggregation, with the differences recorded in the sidecars. ## Oracle -*Class:* **1 (Analytical)** primary, **4 (Invariant)** companion. - -### Applied (Class 1 — Analytical) - -Closed-form derivation: a Bunge ZXZ Euler `(0, Φ, 0)` is a pure rotation about x, yielding `c = R^T · [0, 0, 1] = [0, sin(Φ), cos(Φ)]`. For a cell tilted at `Φ_cell` and a feature average c-axis at `Φ_avg` (same construction), the per-cell misorientation is `|Φ_cell − Φ_avg|` folded to `[0°, 90°]` (the fold is a no-op for all fixtures, which use tilts in `[0°, 25°]`). - -Per-feature finalize: - -- `featAvgCAxisMis[f] = (Σ miso[hex+valid cells in f]) / counts[f]` -- `featStdevCAxisMis[f] = sqrt(Σ (cellRefCAxisMis[i] − featAvg[f])² / counts[f])` — **population stddev** (divisor `counts`, NOT `counts−1`) - -This collapses every expected value in the test fixtures to integer arithmetic. +*Class:* 1 (Analytical), with 4 (Invariant). -### Applied (Class 4 — Invariant) +*Applied:* Pure Bunge rotations `(0, Φ, 0)` give angles `abs(Φ_cell − Φ_avg)` folded to [0°, 90°]; hand calculations give feature means and population standard deviations for the hexagonal fixtures. -| # | Invariant | Algorithm contract / fixture relevance | -|---|---|---| -| I1 | `cellRefCAxisMis[i] ∈ [0.0f, 90.0f]` for hex cells | Fold contract `if(w > 90) w = 180 − w` | -| I2 | `cellRefCAxisMis[i] == 0.0f` for non-hex / invalid cells | Skip-branch explicitly writes 0 (lines 153-156) | -| I3 | `featAvgCAxisMis[f]` equals the arithmetic mean of `cellRefCAxisMis[i]` for hex+valid cells `i ∈ f` | Algorithm formula at line 176 | -| I4 | `featStdevCAxisMis[f]` is the **population** stddev | Algorithm uses `counts[f]` not `counts[f]−1` (line 202) | -| I5 | All-identical-orientation feature → all `cellRefCAxisMis == 0`, `featAvg == 0`, `featStdev == 0` | Self-consistency of the c-axis projection | -| I6 | All-non-hex feature → `featAvg == NaN`, `featStdev == NaN` | **Documents the latent divide-by-zero** at lines 176, 202 (code paths 5 + 7). Load-bearing for the V&V finding. | +*Encoded:* `src/Plugins/OrientationAnalysis/test/ComputeFeatureReferenceCAxisMisorientationsTest.cpp::OrientationAnalysis::ComputeFeatureReferenceCAxisMisorientationsFilter: Class 1 — Simple Hex Triple` and the three companion Class 1/Class 4 cases cover three base fixtures. All pass in CI. -### Encoded - -`test/ComputeFeatureReferenceCAxisMisorientationsTest.cpp` — 4 inlined fixtures in the `AnalyticalFixtures` namespace, all pass: - -- **Class 1:** `Class 1 — Simple Hex Triple` (closed-form 3-cell sanity), `Class 1 — Realistic Microstructure (exposes divide-by-zero)` (5×5×1, 6 features incl. F3 all-cubic), `Class 1 — All-Identical Orientation Feature` (invariant I5 confirmation). -- **Class 4:** `Class 4 — Invariants` (3 SECTIONs: range + per-feature-mean formula + I6 NaN-on-empty). - -All `REQUIRE(actual == Approx(expected).margin(1e-3f))` assertions pass; `std::isnan(...)` assertions pass for the F3 all-non-hex feature. - -### Second-engineer review - -*Skipped — reason:* The closed-form derivation reuses the math already reviewed and signed off during the sibling `ComputeFeatureNeighborCAxisMisalignmentsFilter` V&V cycle (F#6, branch `vv/compute_feature_neighbor_caxis_misalignments`), which used the same pure-Φ Bunge rotation argument with the same `|ΔΦ|` reduction. The Class 4 invariants are standard for a per-feature aggregation. External cross-validation will be obtained via the empirical A/B against the legacy 6.5.171 binary (with root causes proven via a surgically patched local build of the legacy source) — diff-explanation only, not oracle. Per V&V policy line 33, legacy is never a correctness oracle. +*Second-engineer review:* Nathan Young, 2026-06-23. ## Bugs found and fixed -The branch retains the non-hexagonal input correction. +All defects in this table are fixed. The fixes will be in the DREAM3D-NX release after v7.4.2. DREAM3D-NX v7.0.0 contains the fix for `ComputeFeatureReferenceCAxisMisorientationsFilter-D1`. DREAM3D-NX v7.4.2 contains the fix for `ComputeFeatureReferenceCAxisMisorientationsFilter-D3`. -| Deviation | Defect | Affected released versions | Resolution in this branch | -|-----------|--------|----------------------------|---------------------------| -| `ComputeFeatureReferenceCAxisMisorientationsFilter-D1` | Non-hexagonal cells contributed invalid c-axis results. | DREAM.3D 6.5.171; DREAM3D-NX releases before the correction in PR #1438. | Skip non-hexagonal cells and write NaN for features with no valid contributors. | +| Deviation | Defect | Affected released versions | Resolution | +|-----------|--------|----------------------------|------------| +| `ComputeFeatureReferenceCAxisMisorientationsFilter-D1` | Non-hexagonal cells contribute c-axis results that have no meaning under this filter's hexagonal-phase contract. | DREAM.3D v6.5.171 was affected. DREAM3D-NX v7.0.0 through v7.4.2 were not affected. | The filter skips non-hexagonal cells and returns NaN for a feature with no contributing cells. | +| `ComputeFeatureReferenceCAxisMisorientationsFilter-D3` | The algorithm discards its mixed-phase warning before returning. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | The filter returns the result that contains warning `-9803`. | +| `ComputeFeatureReferenceCAxisMisorientationsFilter-D4` | An invalid phase indexes Crystal Structures before validation and can read outside the array. | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was not affected. | The filter checks participation and phase bounds before lookup and rejects an invalid positive participating phase with `-9804`. | ## Code path coverage -**10 of 11 paths exercised** by the V&V test suite. The cancellation path (#8) is structurally present and reviewed but not exercised by an injected cancel signal in the fixtures. - -Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeFeatureReferenceCAxisMisorientations.cpp` (277 lines). - -The algorithm runs in three logical phases: **(a) preflight scan** of `CrystalStructures` to enforce the hex-only contract; **(b) per-cell pass** over the ImageGeometry computing per-cell C-axis misorientation vs. the feature's average and accumulating a per-feature sum; **(c) per-feature finalize** computing the average + population standard deviation. - -| # | Phase | Path | Test case | -|----|-----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------| -| 1 | (a) Preflight | No hex phases in `CrystalStructures` → return error `-9802`. Early exit before any data array is touched. (lines 52-56) | `InValid Filter Execution` (mutates `CrystalStructures[1] = Cubic_High` post-load and asserts the execute fails). | -| 2 | (a) Preflight | Mixed phases (some hex, some non-hex) → push warning `-9803` to `Result<>::warnings()`, proceed. (lines 59-64) | `Class 1 — Realistic Microstructure` (mixed Hex_High + Cubic_High; algorithm-level warning surfaced via `executeResult.result.warnings()`). | -| 3 | (b) Per-cell | Hex Laue (Hex_High or Hex_Low) AND `featureId > 0` AND `cellPhase > 0` → compute `arccos(c1 · avgCAxis)` folded to `[0°, 90°]`, write to per-cell output, accumulate sum + count for the feature. (lines 124-152) | All 4 Class 1 fixtures (every cell with hex phase across `Simple Hex Triple`, `Realistic Microstructure`, `All-Identical Orientation`). | -| 4 | (b) Per-cell | Non-hex OR `featureId == 0` OR `cellPhase == 0` → write `0.0f` to the per-cell output, skip accumulation. (lines 153-156) | `Class 1 — Realistic Microstructure` (F3 cubic cells); `Class 4 — Invariants` sub-section (i) asserts `cellRefCAxisMis == 0` for those cells. | -| 5 | (c) Finalize | Per-feature average `sum / counts` for `featureId ∈ [1, totalFeatures)`. When `counts == 0`, the current implementation writes explicit quiet NaN. (line 176) | All 4 Class 1 fixtures; `Class 4 — Invariants` sub-section (iii) asserts `std::isnan(featAvg[F3])` for the all-non-hex feature. | -| 6 | (c) Finalize | Per-cell population-stddev accumulation: `Σ (cellRefCAxisMis − featAvg)²` over every cell, attributed to its feature. (lines 182-192) | All 4 Class 1 fixtures (stddev compared against closed-form `sqrt(populationVariance)` for F2 + F5 in the Realistic Microstructure). | -| 7 | (c) Finalize | Per-feature stddev finalize: `sqrt(sumSqDiff / counts)`. When `counts == 0`, the current implementation writes explicit quiet NaN. (line 202) | All 4 Class 1 fixtures; `Class 4 — Invariants` sub-section (iii) asserts `std::isnan(featStdev[F3])`. | -| 8 | Cancellation | `m_ShouldCancel` checked at 4 sites: outermost cell z-loop (line 105), per-feature avg loop (169), stddev cell loop (184), stddev finalize loop (197). Early `return {}` skips remaining output. | *Not directly tested — would require cancel-signal injection mid-execution. Structurally reviewed and confirmed to early-return cleanly at each of the 4 sites.* | -| 9 | Slice I/O | Accumulate one feature across slices and reread its cell outputs for variance | `genuine HDF5 slice-boundary oracle` — 10°, 20°, 30°; mean 20° and population standard deviation sqrt(200/3) | -| 10 | Validation | Invalid participating cell phase → -9804 | `Phase Index Bounds` | -| 11 | Preflight | Cell array tuple mismatch → -9800 | `Preflight Error - Cell array tuple count mismatch (-9800)` | - -The all-non-hex feature path (paths 5 + 7) is the load-bearing case for the IEEE 754 NaN-propagation behavior — confirmed by the `Class 4 — Invariants` sub-section (iii) test, which would fail if the algorithm produced a finite value instead of NaN for `counts[F3] == 0`. See the Deviations section for the legacy A/B finding (D1) that surfaced 6.5.171's contrasting "garbage non-hex cell values + non-NaN avg" behavior. - +21 of 29 paths exercised. The count maps assertions in the 13 visible tests to source paths; the table lists the remaining paths and their gaps. + +Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeFeatureReferenceCAxisMisorientations.cpp` (309 lines); `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeFeatureReferenceCAxisMisorientationsFilter.cpp` (205 lines). + +Test names below omit `OrientationAnalysis::ComputeFeatureReferenceCAxisMisorientationsFilter: `. `Realistic Microstructure` refers to `Class 1 — Realistic Microstructure (exposes divide-by-zero)`. + +| # | Phase | Path | Test case | +|---|-------|------|-----------| +| 1 | Preflight | Cell tuple counts differ → error `-9800` | `Preflight Error - Cell array tuple count mismatch (-9800)` | +| 2 | Preflight | Valid input → three output actions and warning `-9801` | `Class 1 — Simple Hex Triple` — valid actions and output values | +| 3 | Execution validation | No hexagonal phase → error `-9802` | `InValid Filter Execution` — checks execution failure; `Empty arrays` — checks `-9802` for empty Crystal Structures | +| 4 | Execution validation | Mixed phases → warning `-9803` | `Realistic Microstructure` — checks a nonempty warning list, not the specific warning | +| 5 | Execution validation | All phases are hexagonal → continue | `Class 1 — Simple Hex Triple` | +| 6 | Cell angle | Hexagonal-High cell → calculate and accumulate angle | `Class 1 — Simple Hex Triple`; `Realistic Microstructure` | +| 7 | Cell angle | Hexagonal-Low cell → calculate and accumulate angle | `Statistics exclude nonparticipating cells` — Hexagonal-High and Hexagonal-Low cells give mean 20° and standard deviation 10° | +| 8 | Cell angle | Non-hexagonal cell → zero output and no contribution | `Realistic Microstructure`; `Class 4 — Invariants` | +| 9 | Cell angle | Feature zero → skip the phase lookup | `Phase Index Bounds` — feature-zero section checks successful execution only | +| 10 | Cell angle | Negative phase → skip the phase lookup | `Phase Index Bounds` — nonpositive-phase section checks successful execution only | +| 11 | Cell angle | Phase zero → skip the angle calculation | `Statistics exclude nonparticipating cells` — checks zero cell output and no contribution to feature statistics | +| 12 | Cell angle | Positive participating phase exceeds the ensemble bounds → error `-9804` | `Phase Index Bounds` — checks error `-9804` | +| 13 | Cell angle | Angle is at most 90° → keep the angle | All three Class 1 cases | +| 14 | Cell angle | Angle is above 90° → replace it with `180° − angle` | *Not directly tested. All analytical angle differences are below 90°.* | +| 15 | Cell angle | Cosine is outside [-1, 1] → clamp it | *Not directly tested. No fixture forces a cosine outside the interval.* | +| 16 | Feature mean | Positive contributor count → divide sum by count | `Class 1 — Simple Hex Triple`; `Realistic Microstructure` | +| 17 | Feature mean | Zero contributor count → explicit quiet NaN | `Realistic Microstructure`; `Class 4 — Invariants` | +| 18 | Variance | Read cell angles again and accumulate squared differences only for positive FeatureIds and positive hexagonal phases | All three Class 1 cases; `Statistics exclude nonparticipating cells` | +| 19 | Feature standard deviation | Positive count → `sqrt(sum of squared differences / count)` | `Class 1 — Simple Hex Triple`; `Realistic Microstructure` | +| 20 | Feature standard deviation | Zero count → explicit quiet NaN | `Realistic Microstructure`; `Class 4 — Invariants` | +| 21 | Feature output | Feature-zero mean and standard deviation remain zero | *Not directly tested. No case asserts both feature-zero outputs.* | +| 22 | Slice transfers | Feature contributions span multiple slices; skipped cells reset to zero | `Large constant-angle feature precision` — checks statistics across two slices; `Statistics exclude nonparticipating cells` — checks skipped-cell zeros across three slices | +| 23 | Cancellation | Stop in either slice pass or either feature loop | *Not directly tested. No fixture injects cancellation.* | +| 24 | Input cache | Crystal Structures or Average C-Axes read fails → return error | *Not directly tested. No fixture injects a cache-read failure.* | +| 25 | First slice pass | FeatureIds, Phases or Quats read fails → return error | *Not directly tested. No fixture injects an input-slice failure.* | +| 26 | Second slice pass | FeatureIds, Phases or cell-angle read fails → return error | *Not directly tested. No fixture injects a second-pass read failure.* | +| 27 | Output | Cell-angle, feature-mean or feature-standard-deviation write fails → return error | *Not directly tested. No fixture injects an output-write failure.* | +| 28 | Preflight | Cell Feature Ids, Cell Phases and Cell Quaternions have equal tuple counts that differ from the selected geometry cell count → error `-9805` | `Geometry cell tuple counts` — all arrays have 4 or 12 tuples for 8 cells; matching arrays in other Attribute Matrices are accepted | +| 29 | Execution validation | FeatureId is negative or exceeds the Average C-Axes tuple bounds → error `-9806` | `Feature Id bounds before statistics` — rejects IDs -1 and 2 even with phase zero; `Empty arrays` — rejects cells with no Average C-Axes tuples | ## Test inventory -- Pre-V&V: 3 TEST_CASEs (1 exemplar-based regression + 1 invalid-execution + 1 SIMPL backwards-compat). -- Current OOC recertification: **8 registered test cases plus 1 hidden OOC contract test**. - -| Test case | Status | Notes | -|--------------------------------------------------------------------|----------------------|-------| -| `Valid Filter Execution` | retired | Circular oracle — consumes `caxis_data.tar.gz` whose `FeatureReferenceCAxisMisorientations` / `FeatureAvgCAxisMisorientations` / `FeatureStdevCAxisMisorientations` arrays were generated from a SIMPLNX run (or a pre-EbsdLib-2.4.1 SIMPLNX run), not from an independent oracle. Per V&V policy line 33, "matches SIMPLNX-then" is not a correctness check. **Archive download stays in `test/CMakeLists.txt`** — `caxis_data.tar.gz` is also consumed by `ComputeCAxisLocationsTest.cpp` (lines 27, 61). | -| `InValid Filter Execution` | kept | Exercises Path 1 (all-non-hex preflight error `-9802`). Mutates `CrystalStructures[1] = 1` (Cubic_High) and asserts execute fails. Re-use unchanged. | -| `SIMPL Backwards Compatibility` | kept | DYNAMIC_SECTION over 6.4 (Filter_Name) + 6.5 (UUID) conversion fixtures at `test/simpl_conversion/6_{4,5}/`. Re-use unchanged. | -| `Class 1 — Simple Hex Triple` | new-for-V&V | Minimal closed-form fixture. 3×1×1 ImageGeom, 1 hex feature (sentinel + F1), cells at `Φ = 0°, 5°, 10°` (Bunge ZXZ `(0, Φ, 0)`). `AvgCAxes[F1]` set to the pre-computed c-axis at `Φ = 5°` (the geometric mean). Expected: `cellRefCAxisMis = [5°, 0°, 5°]`, `featAvg[F1] = 10/3 ≈ 3.333°`, `featStdev[F1] = √(50/9) ≈ 2.357°`. Exercises Paths 3, 5, 6, 7. | -| `Class 1 — Realistic Microstructure (exposes divide-by-zero)` | new-for-V&V | The meaty fixture. 5×5×1 ImageGeom, 6 features (sentinel + 5 real): F1 hex (Φ all 0°), F2 hex (Φ = 8,9,10,11,12°), F3 cubic, F4 hex (Φ all 20°), F5 hex (Φ = 25,28,30,32,35°). Per-feature cell spreads chosen for non-trivial stddev. **F3 is the load-bearing feature**: 0 hex cells → `counts[F3] = 0` → exercises the all-non-hex-feature → NaN path (paths 5, 7) → `featAvg[F3] = NaN`, `featStdev[F3] = NaN` via IEEE 754. Exercises Paths 2, 3, 4, 5, 6, 7. | -| `Class 1 — All-Identical Orientation Feature` | new-for-V&V | Invariant I5 + minimal stddev=0 confirmation. 5×1×1, 1 hex feature, 5 cells all at `Φ = 10°`, `AvgCAxes[F1]` = c-axis at `Φ = 10°`. Expected: `cellRefCAxisMis = [0°, 0°, 0°, 0°, 0°]`, `featAvg[F1] = 0°`, `featStdev[F1] = 0°`. | -| `Class 4 — Invariants` (3 SECTIONs) | new-for-V&V | Reuses the Realistic Microstructure fixture data. Sub-sections: **(i) Range** — every `cellRefCAxisMis[i] ∈ [0°, 90°]` for hex cells, `== 0.0f` for non-hex cells (invariants I1 + I2). **(ii) Per-feature averaging formula** — `featAvg[f]` equals the arithmetic mean of `cellRefCAxisMis[hex+valid cells in f]` (invariant I3). **(iii) All-non-hex feature → NaN** — `featAvg[F3]` and `featStdev[F3]` are both NaN (invariant I6 — **load-bearing for the V&V finding** at paths 5, 7). | -| `Preflight Error - Cell array tuple count mismatch (-9800)` | kept | Rejects unequal cell tuple counts. | -| `Phase Index Bounds` | kept | Checks participating phase bounds and ignored feature-zero cases. | -| `genuine HDF5 slice-boundary oracle` | new-for-V&V | 17×19×4 cells. The algorithm uses 323-cell slice buffers. Contributors on either side of a slice boundary and in the last slice determine all three output arrays. This proves an algorithm block boundary; the physical HDF5 chunk contains the whole small volume. | - - -The hidden test reuses `CreateScaffold`, `SetCellQuat`, and the output helpers. Expected values are analytical: [10°, 20°, 30°], mean 20°, and population standard deviation sqrt(200/3), within 0.001°. It also checks that skipped cells do not retain values from a previous slice. The upstream oracle assertions remain intact. - -### Test scaffolding pattern - -Mirrors the established `AnalyticalFixtures` namespace pattern from sibling `ComputeFeatureNeighborCAxisMisalignmentsTest.cpp` (F#6): - -- `CreateScaffold(nX, nY, nZ, numFeatures, numCrystalStructures)` — builds the in-memory `DataStructure` with an ImageGeom + Cell AM + Feature AM + Ensemble AM and pre-allocates all input/output arrays. -- `QuatFromPhiDeg(phiDeg)` — returns `{sin(phi/2 rad), 0, 0, cos(phi/2 rad)}` (quaternion for Bunge ZXZ `(0, phiDeg, 0)`). -- `CAxisFromPhiDeg(phiDeg)` — returns `{0, sin(phiDeg rad), cos(phiDeg rad)}` (the pre-computed c-axis for the AvgCAxes input). -- `BuildArgs()` — constructs the `Arguments` object with all 9 standard input/output paths. -- `BuildRealisticMicrostructure()` — builds the 5×5×1 6-feature fixture (reused by Fixtures 2 + 4). - -### Pipeline impact note - -`pipelines/EBSD_File_Processing/EBSD_Hexagonal_Data_Analysis.d3dpipeline` runs this filter. No algorithm change was applied during this V&V cycle (the per-feature NaN-on-empty behavior at paths 5 + 7 was confirmed as correct IEEE 754 output by the Class 4 sub-section (iii) test), so the pipeline's output is byte-identical to its pre-V&V SIMPLNX output. Users migrating from DREAM3D 6.5.171 will see deviations D1 + D2 in the pipeline's output — see the Deviations section. - -OOC recertification, 2026-09-18: serial CTest passed 8/8 in `NX-Com-Qt69-Vtk96-Rel` and 8/8 in `NX-Com-Qt69-Vtk96-OoC-Rel`. The new hidden boundary case passed 34 assertions in the OOC binary and is included in the OOC-only `OrientationAnalysisOocStoreContracts` CTest entry. The original report status and sign-off above are historical and unchanged. +The 13 visible cases and one hidden case are in `src/Plugins/OrientationAnalysis/test/ComputeFeatureReferenceCAxisMisorientationsTest.cpp`. + +| Test case | Status | Notes | +|-----------|--------|-------| +| `OrientationAnalysis::ComputeFeatureReferenceCAxisMisorientationsFilter: Class 1 — Simple Hex Triple` | new-for-V&V | Three angles `[5, 0, 5]`; mean `10/3`; population standard deviation `sqrt(50/9)`. | +| `OrientationAnalysis::ComputeFeatureReferenceCAxisMisorientationsFilter: Class 1 — Realistic Microstructure (exposes divide-by-zero)` | new-for-V&V | Checks 25 cell angles, five feature means and five standard deviations. The cubic feature has zero cell angles and NaN feature outputs; the source now writes NaN explicitly. | +| `OrientationAnalysis::ComputeFeatureReferenceCAxisMisorientationsFilter: Class 1 — All-Identical Orientation Feature` | new-for-V&V | Checks five zero cell angles and zero feature mean and standard deviation. | +| `OrientationAnalysis::ComputeFeatureReferenceCAxisMisorientationsFilter: Class 4 — Invariants` | new-for-V&V | Three sections check angle bounds and non-hexagonal zeros, the feature-mean formula, and NaN outputs for the cubic feature. | +| `OrientationAnalysis::ComputeFeatureReferenceCAxisMisorientationsFilter: InValid Filter Execution` | kept | Loads the archive, changes the phase to Cubic-High and checks execution failure and tuple dimensions. | +| `OrientationAnalysis::ComputeFeatureReferenceCAxisMisorientationsFilter: Preflight Error - Cell array tuple count mismatch (-9800)` | kept | Uses 10 FeatureIds/Quats tuples and 9 phase tuples; checks preflight error `-9800`. | +| `OrientationAnalysis::ComputeFeatureReferenceCAxisMisorientationsFilter: SIMPL Backwards Compatibility` | kept | Dynamic sections `SIMPL 6.5 (UUID)` and `SIMPL 6.4 (Filter_Name)` check UUID, comments and converted arguments. This case reads JSON fixtures. | +| `OrientationAnalysis::ComputeFeatureReferenceCAxisMisorientationsFilter: genuine HDF5 slice-boundary oracle` | new-for-V&V | Hidden. A 17×19×4 fixture checks angles `[10, 20, 30]`, mean 20, standard deviation `sqrt(200/3)`, and zero values in skipped cells across slices. | +| `OrientationAnalysis::ComputeFeatureReferenceCAxisMisorientationsFilter: Phase Index Bounds` | kept | Checks `-9804` for a positive phase outside the ensemble and successful execution for a negative phase or feature zero. | +| `OrientationAnalysis::ComputeFeatureReferenceCAxisMisorientationsFilter: Geometry cell tuple counts` | new-for-V&V | Checks `-9805` when all three cell arrays have 4 or 12 tuples for 8 geometry cells. Accepts 8 tuples in other Attribute Matrices; checks `-9800` when each input in turn is short, with array paths and counts in the messages. | +| `OrientationAnalysis::ComputeFeatureReferenceCAxisMisorientationsFilter: Feature Id bounds before statistics` | new-for-V&V | Checks execution error `-9806` for FeatureIds -1 and 2 with two feature tuples, even with phase zero. Checks both array paths in the message and rejection before the cancellation guard. | +| `OrientationAnalysis::ComputeFeatureReferenceCAxisMisorientationsFilter: Statistics exclude nonparticipating cells` | new-for-V&V | Hexagonal-High and Hexagonal-Low angles 10° and 30° give mean 20° and population standard deviation 10°. Checks zero cell outputs for phases 0 and -1, a cubic phase and feature zero; checks NaN statistics for an empty feature. | +| `OrientationAnalysis::ComputeFeatureReferenceCAxisMisorientationsFilter: Large constant-angle feature precision` | new-for-V&V | Two million cells at 13.1° give mean 13.1° and zero population standard deviation within 0.00001°. Checks the first cell angle and feature statistics across two slices. | +| `OrientationAnalysis::ComputeFeatureReferenceCAxisMisorientationsFilter: Empty arrays` | new-for-V&V | Checks zero cell output tuples and NaN feature statistics for empty cells, error `-9806` for empty Average C-Axes, and error `-9802` for empty Crystal Structures. | ## Exemplar archive -- **Archive:** None — the 4 Class 1 + Class 4 V&V fixtures are inlined in `test/ComputeFeatureReferenceCAxisMisorientationsTest.cpp` under the `AnalyticalFixtures` namespace. -- **Retired exemplar:** `caxis_data.tar.gz` (consumed by the pre-V&V `Valid Filter Execution` TEST_CASE — retired 2026-06-10 as a circular oracle). Archive download **retained** in `test/CMakeLists.txt` because `ComputeCAxisLocationsTest.cpp` still consumes it. -- **Provenance sidecar:** `vv/provenance/ComputeFeatureReferenceCAxisMisorientationsFilter.md` — records the closed-form derivation, per-fixture expected outputs, retired-test disposition, and Claude authorship attribution per the LLM-attribution policy. - -## Deviations from DREAM3D 6.5.171 - -**Empirical A/B** (2026-06-10): SIMPLNX vs DREAM3D 6.5.171, on the Realistic Microstructure fixture. Each root cause was proven by applying the corresponding surgical fixes (Eigen + isHex skip; double-precision stddev accumulation) to a local build of the legacy source, after which the legacy output became byte-for-byte identical to SIMPLNX across all 3 output arrays. A/B workspace at `/Users/mjackson/Desktop/FRCAM_AB_Test/`. +- **Archive:** `caxis_data.tar.gz` — input for the invalid-execution case; analytical expectations are in the test source. +- **SHA512:** `56468d3f248661c0d739d9acd5a1554abc700bf136586f698a313804536916850b731603d42a0b93aae47faf2f7ee49d4181b1c3e833f054df6f5c70b5e041dc` +- **Provenance:** `src/Plugins/OrientationAnalysis/vv/provenance/ComputeFeatureReferenceCAxisMisorientationsFilter.md` -Two documented deviations vs 6.5.171 official: +## Deviations from DREAM.3D v6.5.171 -- **`ComputeFeatureReferenceCAxisMisorientationsFilter-D1`** — Legacy 6.5.171 lacks the `isHex` gate; computes c-axis projection misorientation for non-hex cells and produces garbage values + non-NaN feature averages for all-non-hex features. SIMPLNX skips non-hex cells and produces NaN for `counts == 0` features. PR #1438 fix; root cause proven by applying the same fix to a local build of the legacy source. **Recommendation: trust SIMPLNX.** See `vv/deviations/ComputeFeatureReferenceCAxisMisorientationsFilter.md`. -- **`ComputeFeatureReferenceCAxisMisorientationsFilter-D2`** — Precision-class drift `~1e-4°` per cell / `~1e-5°` per feature avg between 6.5.171 (hand-rolled MatrixMath + float32 stddev) and SIMPLNX (Eigen + double stddev). Root cause proven by applying the Eigen + double-precision-stddev fixes to a local build of the legacy source. **Recommendation: trust SIMPLNX.** See `vv/deviations/ComputeFeatureReferenceCAxisMisorientationsFilter.md`. +See `vv/deviations/ComputeFeatureReferenceCAxisMisorientationsFilter.md` for the source evidence, affected users and recommendations. Only Part 1 entries appear below. -For both D1 and D2 the root cause was proven by patching a local build of the legacy source — empirical A/B confirms byte-identical output between the patched legacy build and SIMPLNX. No additional legacy-side action required from this V&V cycle; contact the DREAM3D team for the legacy-parity patch. +| Deviation | Observed difference | +|-----------|---------------------| +| `ComputeFeatureReferenceCAxisMisorientationsFilter-D1` | SIMPLNX skips non-hexagonal cells and returns NaN for features with no contributors. DREAM.3D v6.5.171 computes angles for any positive phase. | +| `ComputeFeatureReferenceCAxisMisorientationsFilter-D2` | Double-precision orientation math and variance accumulation change numerical rounding relative to DREAM.3D v6.5.171. | diff --git a/src/Plugins/OrientationAnalysis/vv/ComputeFeatureReferenceMisorientationsFilter.md b/src/Plugins/OrientationAnalysis/vv/ComputeFeatureReferenceMisorientationsFilter.md index 3c942cc2d3..506b86f34e 100644 --- a/src/Plugins/OrientationAnalysis/vv/ComputeFeatureReferenceMisorientationsFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/ComputeFeatureReferenceMisorientationsFilter.md @@ -1,153 +1,123 @@ # V&V Report: ComputeFeatureReferenceMisorientationsFilter -| | | -|-----------|--------------------------| -| Plugin | OrientationAnalysis | -| SIMPLNX UUID | `24b54daf-3bf5-4331-93f6-03a49f719bf1` | -| SIMPLNX Human Name | Compute Feature Reference Misorientations | -| DREAM3D 6.5.171 equivalent | `FindFeatureReferenceMisorientations` — `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/FindFeatureReferenceMisorientations.{h,cpp}` (UUID `428e1f5b-e6d8-5e8b-ad68-56ff14ee0e8c`) | -| Verified commit | ** | -| Status | COMPLETE | -| Sign-off | *Michael Jackson (V&V cycle completion, 2026-06-01) — algorithm originally translated to SIMPLNX by Nathan Young (PR history)* | +| | | +|--------|--------------| +| Plugin | OrientationAnalysis | +| SIMPLNX UUID | `24b54daf-3bf5-4331-93f6-03a49f719bf1` | +| SIMPLNX Human Name | Compute Feature Reference Misorientations | +| DREAM.3D v6.5.171 equivalent | `FindFeatureReferenceMisorientations` (OrientationAnalysis plugin, UUID `428e1f5b-e6d8-5e8b-ad68-56ff14ee0e8c`) | +| Verified commit | ** | +| Status | COMPLETE | +| Sign-off | Michael Jackson, 2026-06-01; second engineer: Nathan Young, 2026-06-03 | ## At a glance -| Aspect | Current state | -|------------------------|--------------------------| -| Algorithm Relationship | **Port** — same two-mode structure + per-voxel math via `LaueOps`. `QuatF`→`QuatD`; `getMisoQuat`→`calculateMisorientation`; raster→linear iteration; cancel checks added; new optional `EuclideanCenters` array (Mode 1). UUID reassigned for `Find`→`Compute` rename. | -| Oracle (confirmed) | **Class 1 (Analytical) primary** — 6 hand-derived data fixtures covering both reference-orientation modes + 2D + 3D + multi-feature + edge cases. **Class 4 (Invariant) companion** — monotonicity, range bounds, skip-condition correctness, and the per-feature averaging formula asserted via `ClassFourInvariants::AssertClass4Invariants()` across both fixture configurations.| -| Code paths enumerated | 10 of 11 exercised. Cancellation is not directly tested. | -| Tests today | 9 registered tests plus 1 hidden OOC contract test. The latter runs both reference modes over a 65,536-cell block and one-cell tail. | -| Exemplar archive | **None — inline-constructed in test source.** The pre-existing `compute_feature_reference_misorientation.tar.gz` archive (Small-IN100-based regression-against-exemplar) was **retired 2026-06-01** because its exemplar arrays were a circular oracle (regenerated from pre-EbsdLib-2.4.1 SIMPLNX output). The 6 hand-derived data fixtures cover all 8 algorithmic paths and replace the regression-against-archive coverage. | -| Legacy comparison | **Source-inspection comparison against DREAM3D 6.5.171** completed. Algorithm structurally identical to legacy modulo port-time deltas. **No algorithmic deviations** observed (no behavioral bugs in either implementation). One precision-class non-deviation documented: the EbsdLib 2.4.1 `CubicOps::calculateMisorientationInternal` precision improvement (already characterized in `BadDataNeighborOrientationCheckFilter`'s V&V cycle) propagates into per-feature averages for sym-op-aligned grain boundaries — non-observable on data fixtures. | -| Bug flags | None. | -| V&V phase | **All V&V work complete per V2 policy.** Class 1 + Class 4 oracle confirmed against 8-test suite; circular-oracle archive retired; legacy A/B by source inspection; algorithm review applied; user-facing doc updated. Three source-tree deliverables (this report + `vv/deviations/...` + `vv/provenance/...`) are in place. **V&V complete and signed off by Michael Jackson (technical authority), 2026-06-01.** | +| Aspect | Current state | +|--------|---------------| +| Algorithm Relationship | Minor changes to `FindFeatureReferenceMisorientations` in DREAM.3D v6.5.171. SIMPLNX retains both reference modes and adds double-precision orientation math, input checks, block reads and center coordinates. | +| Oracle (confirmed) | Class 1 analytical angles and centers, with Class 4 bounds, skip rules and feature means. Six analytical fixtures and an invariant sweep encode the oracle. | +| Code paths enumerated | 11 of 23 paths exercised. Gaps include cancellation, I/O failures, invalid feature counts and block-boundary cases. | +| Tests today | 10 test cases: six analytical cases, one invariant sweep, one bounds case, one conversion case and one hidden boundary case. The nine default cases run in the supplied CI configuration. | +| Exemplar archive | None. Tests construct inputs and expected values in C++; `compute_feature_reference_misorientation.tar.gz` is retired. | +| Legacy comparison | Not run on the current algorithm. The DREAM.3D v6.5.171 comparison predates the current block-read algorithm. | +| Bug flags | None | +| V&V phase | Source review, analytical fixtures and deviation records are complete. A legacy comparison with the current algorithm remains open. | ## Summary -`ComputeFeatureReferenceMisorientationsFilter` computes the misorientation angle (in degrees) between each cell's quaternion and a per-feature reference quaternion. The reference is either the feature's average quaternion (Mode 0) or the quaternion of the voxel within the feature that is farthest from the grain boundary (Mode 1). Per-feature averages are computed alongside the per-cell values. In Mode 1, a `Feature Euclidean Centers` array recording the coordinates of each feature's reference voxel is also written. - -Verification is via a **Class 1 (Analytical) hand-derived data-fixture set of 6 unit tests** plus a **Class 4 (Invariant) companion sweep**. The fixtures use pure φ1-rotation quaternions (Bunge ZXZ Euler `(φ1, 0, 0)`) so that misorientation values are closed-form derivable: for Δφ1 ∈ {0°, 5°, 10°} and cubic symmetry, the symmetry-reduced misorientation equals `|Δφ1|`. The fixtures range from 2×2×2 (8 voxels, 1 feature) to 4×3×1 (12 voxels, 5 features including a background and an all-unphased feature), and include a 3×3×2 (18-voxel) 3D fixture specifically to verify the linear-voxelIdx → 3D-coord arithmetic in Mode 1. - -A pre-existing `compute_feature_reference_misorientation.tar.gz` archive (Small-IN100 regression-against-exemplar) was retired during this V&V cycle: its exemplar arrays were generated from a pre-EbsdLib-2.4.1 SIMPLNX run (circular oracle), and the EbsdLib 2.4.1 `CubicOps::calculateMisorientationInternal` precision improvement shifted the exemplar values by 2× to 10× the 1e-4 epsilon used in the regression check. The data fixtures cover all 8 algorithmic paths analytically and remove the circular-oracle dependency. Source inspection of the legacy `FindFeatureReferenceMisorientations` confirms the SIMPLNX algorithm is a clean Port with no algorithmic deviations; the only legacy-vs-SIMPLNX difference is the EbsdLib precision improvement (a precision-class non-deviation), which manifests on real EBSD data with cubic-phase sym-op-aligned boundaries but is non-observable on the data fixtures. +`ComputeFeatureReferenceMisorientationsFilter` calculates each cell's misorientation from its feature's average orientation or its farthest-boundary cell orientation. It also calculates feature means and, in the second mode, reference-cell coordinates. Class 1 fixtures and Class 4 invariants verify both modes; the fixed defects are listed below. ## Algorithm Relationship -*Classification:* **Port (with UUID reassignment and API modernization)** ~~| Minor changes | Rewrite | New filter~~ - -*Evidence:* The SIMPLNX algorithm at `Algorithms/ComputeFeatureReferenceMisorientations.cpp` (249 lines) is a near line-by-line translation of legacy `FindFeatureReferenceMisorientations::execute()` (DREAM3D 6.5.171, ~110 lines). Same two-mode dispatch (`ReferenceOrientation` parameter), same per-voxel main loop computing misorientation via `LaueOps`, same per-feature averaging finalization. The SIMPLNX filter was assigned a **new UUID** (`24b54daf-3bf5-4331-93f6-03a49f719bf1` vs legacy `428e1f5b-e6d8-5e8b-ad68-56ff14ee0e8c`) for the `Find` → `Compute` rename; SIMPL 6.4/6.5 pipelines still open correctly via the conversion fixtures at `test/simpl_conversion/6_*/`. +*Classification:* Minor changes -*Port-time deltas (non-deviation — preserve algorithmic equivalence at hand-built-fixture precision):* - -1. **EbsdLib API**: `getMisoQuat` → `calculateMisorientation` (axis + angle returned together; underlying `LaueOps` math identical). -2. **Quaternion precision**: `QuatF` → `QuatD` inside the algorithm (float32 inputs promoted, output cast back to float32). Equivalent for non-precision-sensitive inputs; the EbsdLib 2.4.1 CubicOps fix is visible for cubic sym-op-aligned boundaries (see D1). -3. **Voxel iteration**: `for col/row/plane` triple loop → `for voxelIdx` linear loop. Equivalent iteration order; raster-order tie-break (`>=`, later-voxel-wins) preserved in `m_Centers` selection. -4. **Auxiliary storage**: legacy `avgMisoPtr` (interleaved count + sum) → two separate vectors `avgMisorientationSums` + `avgMisorientationCounts`. Equivalent semantics. -5. **Cancel checks** added (UX only; no algorithmic effect on completed runs). -6. **New optional `EuclideanCenters` array** (Mode 1 only) — SIMPLNX feature, not a port artifact; does not affect pre-existing output. -7. **EbsdLib 2.4.1 CubicOps precision improvement** — non-observable on V&V data fixtures (no sym-op-aligned features); ~ULP-scale per-feature drift on real EBSD data. See D1 + `BadDataNeighborOrientationCheckFilter`'s V&V cycle. - -*Material PRs since baseline (2025-10-01):* None identified that materially change this filter's algorithm. PR #1472 (EbsdLib 2.0.0 API bump) is the closest, and that just affects the `getMisoQuat` → `calculateMisorientation` API delta noted above. +*Evidence:* The DREAM.3D v6.5.171 source and current algorithm share reference selection and feature averaging. SIMPLNX corrects Mode 1, adds reference-cell coordinates, reads data in blocks and checks indices. ## Oracle -*Class:* **1 (Analytical)** primary + **4 (Invariant)** companion. Class 3 (Paper-based) N/A — this filter delegates misorientation math to `ebsdlib::LaueOps::calculateMisorientation`; the Rowenhorst 2015 paper-based verification of that math is part of EbsdLib's own V&V, not this filter's. - -### Applied (Class 1 — Analytical) - -Expected per-voxel `FRM` and per-feature `avgRefMis` outputs are derived in closed form from the input `Quats` + `Phases` + `FeatureIds` + reference-quaternion source (Mode 0: `AvgQuats[fid]`; Mode 1: `Quats[centerVoxelIdx]`) by hand-tracing the algorithm. The fixtures use pure φ1-rotation quaternions (Bunge ZXZ Euler `(φ1, 0, 0)`) so that misorientation between any two voxels equals `|Δφ1|` modulo the cubic c-axis 4-fold symmetry. For Δφ1 ∈ {0°, 5°, 10°} the symmetry reduction is the identity (no fold below the input), so expected FRM values are exactly `|Δφ1|`. Per-feature averages are `sum(FRM[v ∈ feature fid, phase>0]) / count(v ∈ feature fid, phase>0)` for `fid > 0` with non-empty count, or `0` when count is empty (path 7 in the code-path table below). - -Mode 1 hand-picks `GBEuclideanDistances` values so that `m_Centers[fid]` selection has a unique closed-form answer (or, for the tied-distance multi-feature fixture, a deterministic later-voxel-wins tie-break per the `>=` comparison semantics that both legacy and SIMPLNX share). - -### Applied (Class 4 — Invariant) - -Five invariants every filter run must satisfy regardless of input configuration, asserted via `namespace AnalyticalFixtures::AssertClass4Invariants()` in the test source: - -- **Non-negativity**: `FRM[i] >= 0` ∀ voxel -- **Cubic max-angle bound**: `FRM[i] <= 62.8°` for cubic phases (the maximum symmetry-reduced misorientation under m-3m symmetry) -- **Skip-path correctness**: `FRM[i] == 0` when `featureIds[i] == 0` OR `cellPhases[i] == 0` -- **Background-feature zero**: `avgRefMis[0] == 0` (background) -- **Per-feature averaging formula**: `avgRefMis[fid] == sum(FRM[v ∈ feature fid, phase>0]) / count(v ∈ feature fid, phase>0)` for `fid > 0` with count > 0; `avgRefMis[fid] == 0` when count == 0 - -### Encoded +*Class:* 1 (Analytical), with 4 (Invariant). -- **Class 1 (Analytical)**: `test/ComputeFeatureReferenceMisorientationsTest.cpp` — 6 `TEST_CASE` blocks under the `Class 1 - …` family. Per-voxel and per-feature expected values asserted via `AnalyticalFixtures::RequireFRMClose()` / `RequireAvgClose()` with 1e-3° tolerance (degrees) and `Approx().margin(1e-5f)` for `EuclideanCenters` coord assertions. -- **Class 4 (Invariant)**: `ComputeFeatureReferenceMisorientationsFilter: Class 4 - Invariants Sweep` — two configurations (Mode 0 mixed 3×3×1 and Mode 1 3×3×1) each asserting all five invariants via the `AssertClass4Invariants()` helper. -- *(kept)* `ComputeFeatureReferenceMisorientationsFilter: SIMPL Backwards Compatibility` — SIMPL 6.4 + 6.5 conversion paths via `DYNAMIC_SECTION`; UUID + argument-key + parameter-value validation only. +*Applied:* Pure rotations about the cubic c-axis give known angles; literal cell positions give centers, and participating-cell sums divided by counts give feature means. -### Second-engineer review +*Encoded:* `src/Plugins/OrientationAnalysis/test/ComputeFeatureReferenceMisorientationsTest.cpp` — six `Class 1 - ...` fixtures and `Class 4 - Invariants Sweep`; nine default test cases run in CI. All pass in CI. -*Pending — recommend an OA-domain engineer (Joey Kleingers or similar) review:* -- *The Class 1 hand-derivations in the 6 data fixtures + 1 invariants sweep for plausibility (the fixtures are small enough to walk through in ~30 minutes).* -- *The Class 4 invariant set for completeness — are there other properties this algorithm must satisfy?* -- *The decision to retire the `compute_feature_reference_misorientation.tar.gz` Small-IN100 exemplar archive in favor of inline data fixtures.* +*Second-engineer review:* Nathan Young, 2026-06-03. ## Bugs found and fixed -None in the filter during this recertification. The existing EbsdLib precision correction remains documented in D1. +All defects in this table are fixed. The fixes will be in the DREAM3D-NX release after v7.4.2. DREAM3D-NX v7.4.1 contains the fix for `ComputeFeatureReferenceMisorientationsFilter-D2` and `ComputeFeatureReferenceMisorientationsFilter-D3`. DREAM3D-NX v7.4.2 contains the fix for `ComputeFeatureReferenceMisorientationsFilter-D4`. -## Code path coverage - -10 of 11 paths exercised. Cancellation is not directly tested. - -Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeFeatureReferenceMisorientations.cpp` (249 lines). +| Deviation | Defect | Affected released versions | Resolution | +|-----------|--------|----------------------------|---------------------------| +| `ComputeFeatureReferenceMisorientationsFilter-D2` | Mode 1 accessed the average-quaternion input even when that mode did not supply it. | DREAM3D-NX v7.0.0 through v7.4.0. DREAM.3D v6.5.171 was affected. | The filter obtains the feature count from the selected feature matrix when average quaternions are absent. | +| `ComputeFeatureReferenceMisorientationsFilter-D3` | Mode 1 used a cell index to read the feature-average quaternion array, causing wrong values or an invalid access. | DREAM3D-NX v7.0.0 through v7.4.0. DREAM.3D v6.5.171 was not affected. | The filter reads the selected cell's quaternion from the cell array. | +| `ComputeFeatureReferenceMisorientationsFilter-D4` | SIMPL conversion selected the cell matrix as the feature matrix. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | The filter derives the feature matrix from the legacy AvgQuats path. | +| `ComputeFeatureReferenceMisorientationsFilter-D5` | A participating phase could read beyond Crystal Structures. | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was affected. | The filter returns `-34901` before the lookup. | +| `ComputeFeatureReferenceMisorientationsFilter-D6` | An invalid crystal-structure value could index beyond the orientation operators. | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was affected. | The filter returns `-34902` before the operator lookup. | -The algorithm has three logical phases: (a) Mode 1 pre-loop (populate `centers[fid]` from `gbEuclideanDistances` + write `EuclideanCenters` coords); (b) main per-voxel loop (compute per-voxel misorientation, accumulate per-feature sums + counts); (c) per-feature finalize (compute per-feature average from sum/count). - -| # | Phase | Path | Test case | -|----|-------------------|-------------------------------------------------------------------------------------------------------------------|----------------------------------------| -| 1 | (b) Main loop | Mode 0 — `q2 = avgQuatsPtr[featureId * 4 .. ]` | Fixtures A, B, C (`Class 1 - Mode 0 SingleGrainIdentity` / `KnownAngle5deg` / `MultiGrain EdgeCases`) | -| 2 | (a) Mode 1 pre-1 | Mode 1 — first sweep populates `centers[fid]` via `>=` tie-break against `gbEuclideanDistances` | Fixtures D, E, F (`Mode 1 KnownCenter` / `MultiGrain CenterIsolation` / `3D Volume`) | -| 3 | (a) Mode 1 pre-2 | Mode 1 — second sweep writes `EuclideanCenters[fid] = imageGeom.getCoordsf(centers[fid])` | Fixtures D, E, F | -| 4 | (b) Main loop | `featureIds[i] > 0 && cellPhases[i] > 0` → compute misorientation, accumulate `sums[fid]++` and `counts[fid]+=` | All Class 1 fixtures | -| 5 | (b) Main loop | Skip (`featureIds[i] == 0` OR `cellPhases[i] == 0`) → FRM stays 0 (initial `fill(0.0f)` value) | Fixture C (`Mode 0 MultiGrain EdgeCases`) — background voxel 0, mid-feature unphased voxels 6-7, entire feature 4 unphased voxels 8-11| -| 6 | (b) Main loop | Cancel check at loop top (`m_ShouldCancel.load()` → early return) | *Not directly tested.* Unconditional check at loop top in both passes; failure mode (silent cancel-disregard) would manifest as test hang in any test, but is not specifically exercised. Low-value gap. | -| 7 | (c) Finalize | `avgMisorientationCounts[fid] == 0` → `avgRefMis[fid] = 0` | Fixture C — feature 4 has all-unphased voxels, so `count[4] == 0` | -| 8 | (c) Finalize | Otherwise → `avgRefMis[fid] = sums[fid] / counts[fid]` | All Class 1 fixtures with non-empty features | -| 9 | Validation | Invalid participating phase or Laue index | `Phase and Laue Index Bounds` — both reference modes | -| 10 | Mode 1 | Last-equal-distance center selection across a block boundary | `genuine HDF5 65536-block tail oracle` — center at x=65536.5 | -| 11 | Main loop | Full 65,536-cell block and one-cell tail in both reference modes | `genuine HDF5 65536-block tail oracle` — literal cell angles and feature means | +## Code path coverage +11 of 23 paths exercised. Each uncovered path and its reason appear below. + +Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeFeatureReferenceMisorientations.cpp` (249 lines); `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeFeatureReferenceMisorientationsFilter.cpp` (261 lines). + +Test names below omit the `OrientationAnalysis::ComputeFeatureReferenceMisorientationsFilter: ` prefix. + +| # | Phase | Path | Test case | +|---|-------|------|-----------| +| 1 | Reference | Mode 0 uses the feature-average quaternion. | `Class 1 - Mode 0 SingleGrainIdentity`; `Class 1 - Mode 0 KnownAngle5deg` | +| 2 | Reference | Mode 1 selects the greatest distance; the later equal-distance cell wins. | `Class 1 - Mode 1 KnownCenter`; `Class 1 - Mode 1 MultiGrain CenterIsolation` | +| 3 | Centers | Write the selected cell coordinates in 2D and 3D. | `Class 1 - Mode 1 KnownCenter`; `Class 1 - Mode 1 3D Volume` | +| 4 | Cells | Positive feature and phase compute angles, sums and counts. | `Class 1 - Mode 0 KnownAngle5deg`; `Class 1 - Mode 1 KnownCenter` | +| 5 | Cells | Feature zero or phase zero leaves a zero cell angle. | `Class 1 - Mode 0 MultiGrain EdgeCases` | +| 6 | Cancellation | Stop during center selection or cell processing. | *Not directly tested. No fixture injects cancellation.* | +| 7 | Finalize | A feature has no participating cells; its mean is zero. | `Class 1 - Mode 0 MultiGrain EdgeCases` | +| 8 | Finalize | A nonempty feature gets sum/count; background stays zero. | `Class 1 - Mode 0 MultiGrain EdgeCases`; `Class 1 - Mode 1 MultiGrain CenterIsolation` | +| 9 | Validation | Participating phase exceeds the ensemble bounds; return `-34901`. | `Phase and Laue Index Bounds` — both modes | +| 10 | Reference | A distance tie crosses a 65,536-cell block boundary. | *Not directly tested. Only the hidden `genuine HDF5 65536-block tail oracle` targets this boundary.* | +| 11 | Cells | Process a full 65,536-cell block and a one-cell tail. | *Not directly tested. Only the hidden `genuine HDF5 65536-block tail oracle` supplies this size.* | +| 12 | Validation | Participating Laue index exceeds the operator bounds; return `-34902`. | `Phase and Laue Index Bounds` — both modes | +| 13 | Cells | Negative phase skips angle calculation. | *Not directly tested. No fixture assigns a negative phase.* | +| 14 | Validation | Reject invalid FeatureIds before feature-array access. | *Not directly tested. No fixture supplies a negative or out-of-range FeatureId.* | +| 15 | Validation | Neither feature source supplies tuples; return `-34900`. | *Not directly tested. Every fixture supplies feature tuples.* | +| 16 | Reference | Mode 1 obtains the feature count without an AvgQuats array. | *Not directly tested. The shared scaffold creates AvgQuats in both modes.* | +| 17 | Validation | Ignore an invalid phase on feature zero. | `Phase and Laue Index Bounds` — successful execution in both modes | +| 18 | Input | Ensemble or average-quaternion cache read fails. | *Not directly tested. No fixture injects a cache read failure.* | +| 19 | Reference | Feature-ID, distance or center-quaternion read fails. | *Not directly tested. No fixture injects a center-selection read failure.* | +| 20 | Cells | Feature-ID, phase or quaternion read, or angle write, fails. | *Not directly tested. No fixture injects a cell I/O failure.* | +| 21 | Reference | No center candidate reaches zero distance; the default index remains zero. | *Not directly tested. Every Mode 1 feature has a qualifying center.* | +| 22 | Conversion | Convert legacy Mode 0 parameters and UUID. | `SIMPL Backwards Compatibility` — `SIMPL 6.5 (UUID)` and `SIMPL 6.4 (Filter_Name)` | +| 23 | Conversion | Convert Mode 1 with distinct cell and feature matrices. | *Not directly tested. Both conversion fixtures use Mode 0 and the same parent for all array paths.* | ## Test inventory -| Test case | Status | Notes | -|--------|-------------|--| -| `ComputeFeatureReferenceMisorientationsFilter: Class 1 - Mode 0 SingleGrainIdentity` | new-for-V&V | 2×2×2; single feature; all identity quats; expected FRM=0, avg=0. Class 4 invariants asserted. | -| `ComputeFeatureReferenceMisorientationsFilter: Class 1 - Mode 0 KnownAngle5deg` | new-for-V&V | 2×2×2; single feature; all voxel quats = 5° about z, AvgQuats[1] = identity; expected FRM=5° everywhere, avg=5°. Verifies non-zero misorientation magnitude. Class 4 invariants asserted. | -| `ComputeFeatureReferenceMisorientationsFilter: Class 1 - Mode 0 MultiGrain EdgeCases` | new-for-V&V | 4×3×1; 5 features (background + 4 grains); covers skip paths (background, mid-feature unphased), zero-count finalize path (feature 4 all-unphased → avg=0), and normal accumulation. Class 4 invariants asserted. | -| `ComputeFeatureReferenceMisorientationsFilter: Class 1 - Mode 1 KnownCenter` | new-for-V&V | 3×3×1; single feature; center voxel hand-picked via unique max `GBEuclideanDistances`; verifies `centers[]` selection + `EuclideanCenters` coord writing. Class 4 invariants asserted. | -| `ComputeFeatureReferenceMisorientationsFilter: Class 1 - Mode 1 MultiGrain CenterIsolation` | new-for-V&V | 2×3×1; 2 features; verifies `centers[fid]` isolation per feature + tied-distance `>=` later-voxel-wins tie-break. Class 4 invariants asserted. | -| `ComputeFeatureReferenceMisorientationsFilter: Class 1 - Mode 1 3D Volume` | new-for-V&V | 3×3×2; single feature; verifies linear `voxelIdx → (x,y,z)` arithmetic when `dimZ > 1`. Class 4 invariants asserted. | -| `ComputeFeatureReferenceMisorientationsFilter: Class 4 - Invariants Sweep` | new-for-V&V | Runs Mode 0 and Mode 1 configurations distinct from the value-specific fixtures; asserts only the Class 4 invariants. Catches future regressions where specific values shift but invariants still hold. | -| `ComputeFeatureReferenceMisorientationsFilter: SIMPL Backwards Compatibility` | kept | `DYNAMIC_SECTION` over SIMPL 6.4 + 6.5 conversion fixtures (`test/simpl_conversion/6_*/ComputeFeatureReferenceMisorientationsFilter.json`); validates UUID + argument-key + parameter-value decoding. | -| *(retired)* `ComputeFeatureReferenceMisorientationsFilter_AverageMisorientation` | retired | Removed 2026-06-01. Regression-against-archive test consuming `compute_feature_reference_misorientation.tar.gz` exemplar arrays; archive's exemplar values were a circular oracle (regenerated from pre-EbsdLib-2.4.1 SIMPLNX output). Test failure surfaced when EbsdLib 2.4.1 CubicOps precision fix shifted exemplar values by 2× epsilon. Replaced by inline Class 1 + Class 4 fixtures above. | -| *(retired)* `ComputeFeatureReferenceMisorientationsFilter_EuclideanDistance` | retired | Same as above for Mode 1; archive exemplar shifted by 10× epsilon post-EbsdLib-2.4.1.| -| `Phase and Laue Index Bounds` | kept | Invalid participating phase/Laue indices and ignored feature zero in both modes. | -| `genuine HDF5 65536-block tail oracle` | new-for-V&V | Both modes use HDF5 inputs and outputs. Mode 0 expects [15°, 30°] and mean 22.5°. Mode 1 selects the later tied center and expects [15°, 0°] and mean 7.5°, plus literal center coordinates. | - - -The shared scaffold now bulk-initializes the same zero/identity input values. Upstream/develop oracle expectations and tolerances are preserved. The hidden test uses direct `Approx` assertions and does not derive its expected values from filter output. - -The current default selection contains nine tests. The hidden test requires the OOC manager and runs separately. The existing D1 precision disposition remains unchanged; no fresh legacy binary comparison is claimed. - -OOC recertification, 2026-09-18: serial CTest passed 9/9 in `NX-Com-Qt69-Vtk96-Rel` and 9/9 in `NX-Com-Qt69-Vtk96-OoC-Rel`. The new hidden boundary case passed 63 assertions in the OOC binary and is included in the OOC-only `OrientationAnalysisOocStoreContracts` CTest entry. The original report status and sign-off above are historical and unchanged. +The nine default test cases run in CI. All pass in CI. + +| Test case | Status | Notes | +|-----------|--------|-------| +| `OrientationAnalysis::ComputeFeatureReferenceMisorientationsFilter: Phase and Laue Index Bounds` | kept | Both modes reject invalid participating phase/Laue values and ignore an invalid phase on feature zero. | +| `OrientationAnalysis::ComputeFeatureReferenceMisorientationsFilter: SIMPL Backwards Compatibility` | kept | Checks UUID, comments and arguments in `SIMPL 6.5 (UUID)` and `SIMPL 6.4 (Filter_Name)`. Both fixtures use Mode 0 and identical parent paths; they do not isolate D4. | +| `OrientationAnalysis::ComputeFeatureReferenceMisorientationsFilter: genuine HDF5 65536-block tail oracle` | new-for-V&V | Hidden. Requires HDF5 stores. Literal angles are [15°,30°] in Mode 0 and [15°,0°] in Mode 1; means are 22.5° and 7.5°. The later tied center is (65536.5,0.5,0.5). | +| `OrientationAnalysis::ComputeFeatureReferenceMisorientationsFilter: Class 1 - Mode 0 SingleGrainIdentity` | new-for-V&V | 2×2×2 identity orientations; all cell angles and feature means are zero. Checks Class 4 invariants. | +| `OrientationAnalysis::ComputeFeatureReferenceMisorientationsFilter: Class 1 - Mode 0 KnownAngle5deg` | new-for-V&V | 2×2×2; all cells are 5° from the identity reference. Checks angles, mean and Class 4 invariants. | +| `OrientationAnalysis::ComputeFeatureReferenceMisorientationsFilter: Class 1 - Mode 0 MultiGrain EdgeCases` | new-for-V&V | 4×3×1; checks background, unphased cells, an empty-count feature, literal angles and means, and Class 4 invariants. | +| `OrientationAnalysis::ComputeFeatureReferenceMisorientationsFilter: Class 1 - Mode 1 KnownCenter` | new-for-V&V | 3×3×1; checks the selected center (1.5,1.5,0.5), cell angles, feature mean and Class 4 invariants. | +| `OrientationAnalysis::ComputeFeatureReferenceMisorientationsFilter: Class 1 - Mode 1 MultiGrain CenterIsolation` | new-for-V&V | 2×3×1; checks two independent centers, the later-cell tie rule, literal angles, means and Class 4 invariants. | +| `OrientationAnalysis::ComputeFeatureReferenceMisorientationsFilter: Class 1 - Mode 1 3D Volume` | new-for-V&V | 3×3×2; checks the center (1.5,1.5,1.5), angles, mean and Class 4 invariants. | +| `OrientationAnalysis::ComputeFeatureReferenceMisorientationsFilter: Class 4 - Invariants Sweep` | new-for-V&V | Two configurations check nonnegative angles, the cubic angle bound, skip rules, background zero and feature means. | ## Exemplar archive -**None — data inlined in `test/ComputeFeatureReferenceMisorientationsTest.cpp` namespace `AnalyticalFixtures`.** - -The pre-existing `compute_feature_reference_misorientation.tar.gz` archive was retired during this V&V cycle. See `src/Plugins/OrientationAnalysis/vv/provenance/ComputeFeatureReferenceMisorientationsFilter.md` for the retirement rationale and the methodology used to construct the replacement data fixtures. - -- **Archive:** None -- **SHA512:** N/A +- **Archive:** None. Inputs and oracles are inline; `compute_feature_reference_misorientation.tar.gz` is retired. +- **SHA512:** Not applicable. The current plugin `test/CMakeLists.txt` has no registration for this archive. - **Provenance:** `src/Plugins/OrientationAnalysis/vv/provenance/ComputeFeatureReferenceMisorientationsFilter.md` -## Deviations from DREAM3D 6.5.171 - -One precision-class non-deviation documented; no algorithmic deviations. +## Deviations from DREAM.3D v6.5.171 -### ComputeFeatureReferenceMisorientationsFilter-D1 +See `vv/deviations/ComputeFeatureReferenceMisorientationsFilter.md` for source evidence, comparison artifacts and recommendations. -- **Symptom:** Per-feature average misorientations differ from DREAM3D 6.5.171 output by 2× to 10× the 1e-4 epsilon used by `CompareDataArrays` on Small-IN100-class EBSD data. On the V&V data fixtures (pure φ1 rotations, no sym-op-aligned boundaries), no observable deviation. -- **Root cause:** Precision — propagation of the EbsdLib 2.4.1 `CubicOps::calculateMisorientationInternal` precision fix. This filter is a clean Port of the legacy algorithm; no algorithmic deviation. See `vv/deviations/ComputeFeatureReferenceMisorientationsFilter.md` for full root-cause walkthrough. +| Deviation | Observed difference | +|-----------|---------------------| +| `ComputeFeatureReferenceMisorientationsFilter-D1` | Orientation precision and accumulation order change cell angles and feature means. | +| `ComputeFeatureReferenceMisorientationsFilter-D2` | DREAM.3D v6.5.171 crashes in direct Mode 1; current SIMPLNX obtains the feature count without the Mode 0 input. | +| `ComputeFeatureReferenceMisorientationsFilter-D7` | SIMPLNX adds the selected cell's coordinates as a Mode 1 feature array. | diff --git a/src/Plugins/OrientationAnalysis/vv/ComputeIPFColorsFilter.md b/src/Plugins/OrientationAnalysis/vv/ComputeIPFColorsFilter.md index 33bb2598ca..5e295a158f 100644 --- a/src/Plugins/OrientationAnalysis/vv/ComputeIPFColorsFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/ComputeIPFColorsFilter.md @@ -1,127 +1,127 @@ # V&V Report: ComputeIPFColorsFilter -| | | -|-----------|--------------------------| -| Plugin | OrientationAnalysis | +| | | +|--------|--------------| +| Plugin | OrientationAnalysis | | SIMPLNX UUID | `64cb4f27-6e5e-4dd2-8a03-0c448cb8f5e6` | | SIMPLNX Human Name | Compute IPF Colors | -| DREAM3D 6.5.171 equivalent | `GenerateIPFColors` (SIMPL UUID `a50e6532-8075-5de5-ab63-945feb0de7f7`) — `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/GenerateIPFColors.{h,cpp}` | +| DREAM.3D v6.5.171 equivalent | `GenerateIPFColors` (OrientationAnalysis plugin, SIMPL UUID `a50e6532-8075-5de5-ab63-945feb0de7f7`) | | Verified commit | ** | -| Status | COMPLETE | -| Sign-off | Michael Jackson — 2026-07-16 | +| Status | COMPLETE | +| Sign-off | Michael Jackson, 2026-07-09; second engineer: Matthew Marine, 2026-07-13 | ## At a glance -| Aspect | Current state | -|------------------------|--------------------------| -| Algorithm Relationship | **Port** of DREAM3D 6.5.171 `GenerateIPFColors`. The per-cell loop is a line-for-line translation; deltas are the color library (OrientationLib → EbsdLib), a new `Color Key` choice (TSL/PUCM/Nolze-Hielscher; legacy was TSL-only), bool-or-uint8 mask (legacy bool-only), and added cancel checks. | -| Oracle (confirmed) | **Class 1 + 4** (orchestration: mask→black, invalid crystal structure→black, refDir normalization, phase-out-of-range→`-48000`, output invariants) with **Class 2** (each colored cell == a direct in-process EbsdLib `generateIPFColor` call) and **Class 3** (identity cubic viewed down [001] = red IPF corner). 8 tests in `test/ComputeIPFColorsTest.cpp`, all pass in-core and OOC. | -| Code paths enumerated | 17 of 19 exercised. The 2 gaps are the mid-loop cancel branch and the unreachable `-23510` color-key default. | -| Tests today | 9 registered cases plus one hidden HDF5 boundary case. Both serial CTest selections pass 9/9. | -| Exemplar archive | **None for this filter** — the oracle dataset is built inline in C++. The legacy-produced `so3_cubic_high_ipf_001.tar.gz` was **retired as a circular oracle** from this test (it is still downloaded for `CreateEnsembleInfoTest`, so the `download_test_data()` line remains). | -| Legacy comparison | **Run — SIMPLNX vs DREAM3D 6.5.171 (TSL).** SIMPLNX is byte-identical to the stored legacy `IPF Colors` (0/343,963); vs a fresh 6.5.171 run, 14/343,963 cells (0.004%) differ by exactly ±1/255 in one channel. One deviation: `ComputeIPFColorsFilter-D1` (precision + library, quantization jitter). | -| Bug flags | None. | -| V&V phase | Historical status and sign-off retained. Section 4.3 recertification adds independent HDF5 boundary evidence and paired runtime checks. | +| Aspect | Current state | +|--------|---------------| +| Algorithm Relationship | Port of DREAM.3D v6.5.171 `GenerateIPFColors`. SIMPLNX adds two color keys, uint8 masks, negative-phase validation and storage-based execution. | +| Oracle (confirmed) | Classes 1 and 4 check analytical colors and invariants; Class 2 checks routing through EbsdLib on a six-cell fixture. All pass in CI. | +| Code paths enumerated | 19 of 34 exercised. Gaps include cancellation, I/O failures, boundary output checks and concurrent error counting. | +| Tests today | 10 test cases: 9 regular cases and 1 hidden HDF5 boundary case. The regular cases check colors, masks, normalization, input errors and SIMPL conversion. | +| Exemplar archive | `so3_cubic_high_ipf_001.tar.gz` supplies the legacy comparison input. This filter's tests use inline fixtures instead of its stored colors. | +| Legacy comparison | Not run for the current algorithm; the last comparison was on 2026-09-17, before the algorithm changed. | +| Bug flags | None | +| V&V phase | Analytical checks, routing checks and error regressions are encoded. All pass in CI. | ## Summary -`ComputeIPFColorsFilter` assigns each cell an inverse-pole-figure RGB color by delegating the color computation to EbsdLib `LaueOps::generateIPFColor()` for that cell's phase/crystal structure, with optional masking (bad cells → black) and a selectable color key. Because the color math is EbsdLib's (and is covered by EbsdLib's own `TSLColorKeyTest` / `PUCMColorKeyTest` / `NolzeHielscherColorKeyTest` / `ColorKeyKindTest`), V&V here targets only the SIMPLNX **value-add** — per-cell dispatch/indexing, masking, reference-direction normalization, phase-bounds handling, and output packing — using a Class 1/4 analytical-orchestration oracle, a Class 2 in-process EbsdLib cross-check, and a Class 3 standard-IPF-corner anchor. SIMPLNX matched the oracle on the first run (no filter bugs found) and is byte-identical to the stored legacy reference; the only difference vs a fresh 6.5.171 build is ±1/255 quantization jitter on 0.004% of cells (deviation D1). +`ComputeIPFColorsFilter` assigns RGB inverse-pole-figure colors from each cell's Euler angles and phase, with an optional mask and three color keys. Analytical expectations, invariants and direct EbsdLib calls check the filter's routing and output. The V&V records two fixed defects: concurrent error counting and negative phase indices. ## Algorithm Relationship -*Classification:* **Port** +*Classification:* Port -*Evidence:* The SIMPLNX algorithm `Algorithms/ComputeIPFColors.cpp` (203 lines) is a direct translation of `GenerateIPFColors::execute()` / `GenerateIPFColorsImpl::convert()` from DREAM3D 6.5.171. The SIMPL UUID `a50e6532-8075-5de5-ab63-945feb0de7f7` is retained via `OrientationAnalysisLegacyUUIDMapping.hpp` plus SIMPL 6.4/6.5 conversion fixtures. The control flow is preserved verbatim: per-cell init-to-black → mask lookup → phase-bounds sanity → `phase < numPhases && calcIPF && crystalStructures[phase] < LaueGroupEnd` coloring guard → `generateIPFColor` → RGB write, followed by the post-loop `-48000` phase-warning error. - -*Port-time deltas (each with its effect on output):* - -1. **Color library: OrientationLib → EbsdLib.** The `generateIPFColor` call now resolves to EbsdLib. Same standard conventions; produces the ±1/255 quantization jitter documented in `ComputeIPFColorsFilter-D1`. -2. **`Color Key` choice added (PR #1631).** New `ChoicesParameter` routing to `ebsdlib::ColorKeyKind` {TSL, PUCM, Nolze-Hielscher}. Legacy was TSL-only; the default (index 0 = TSL) reproduces legacy behavior. PUCM/Nolze-Hielscher have no legacy equivalent. -3. **Mask accepts bool *or* uint8.** Legacy required a `bool` mask; SIMPLNX `run()` dispatches `convert`/`convert`. No effect on output values; widens accepted input. -4. **Cancel checks added.** `shouldCancel()` guard at the top of the per-cell loop. UX-only. -5. **Reference direction normalization** is unchanged in intent (`FloatVec3::normalize()` vs legacy `MatrixMath::Normalize3x1`); both make the reference direction unit length before use. - -*Material PRs since baseline:* #1631 ("EbsdLib 3.0.0 + V&V of 6 Filters", added the Color Key), #1472 (EbsdLib 2.0.0 API), #1438 (microtexture cleanup), #1501 (Vec3 unification). None alter the coloring logic beyond the deltas above. - -OOC source: `Algorithms/ComputeIPFColorsScanline.cpp` (189 lines). The new test targets the storage path documented below. +*Evidence:* `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/OrientationAnalysisLegacyUUIDMapping.hpp` maps the legacy UUID to this filter; the per-cell mask, crystal-structure and RGB operations follow `GenerateIPFColors`, with EbsdLib replacing OrientationLib. ## Oracle -*Class:* **1 (Analytical)** + **4 (Invariant)** primary, **2 (Reference — EbsdLib)** and **3 (Paper/standard-IPF)** companions. +*Class:* 1 (Analytical), 2 (Reference — EbsdLib routing) and 4 (Invariant). -Per the "test the value-add, not upstream" principle: EbsdLib is the trusted reference for the color math (it has its own test suite), so the oracle verifies that SIMPLNX *routes data correctly into and out of EbsdLib*, not that the color algorithm is correct. +*Applied:* A six-cell cubic/hexagonal fixture checks black masked and invalid-structure cells, the cubic red corner, exact EbsdLib RGB values, output shape and direction normalization. -*Applied:* -- **Class 1 (Analytical, orchestration):** a masked-off cell is exactly `(0,0,0)`; a cell whose crystal structure is `≥ LaueGroupEnd` (999/Unknown) is exactly `(0,0,0)`; a non-unit reference direction `[0,0,5]` yields identical output to `[0,0,1]` (normalization); a cell whose phase index `≥ numPhases` produces error `-48000`. These are pure filter logic, derivable without reference to any DREAM3D implementation. -- **Class 4 (Invariant):** output is a 3-component uint8 array with the parent tuple count; non-colored cells are exactly black; colored cells are non-black. -- **Class 2 (Reference, EbsdLib):** for every colored cell, the filter output equals a direct, independent in-process `ebsdlib::LaueOps::GetAllOrientationOps()[crystalStruct]->generateIPFColor(euler, refDir, false, kind)` call — verifying the phase→crystal-structure→ops indexing, the 3-tuple Euler slicing, the reference-direction hand-off, and the color-key pass-through. -- **Class 3 (Standard IPF):** an identity-orientation (`(0,0,0)`) cubic cell viewed down `[001]` is the red corner of the standard IPF triangle. EbsdLib's own `TSLColorKeyTest` fixes `[001] → r=1, g=0, b=0`; the test asserts R ≥ 250, G = 0, B = 0. +*Encoded:* `src/Plugins/OrientationAnalysis/test/ComputeIPFColorsTest.cpp::OrientationAnalysis::ComputeIPFColorsFilter: Class 1/2/3 Oracle (inline analytical dataset)` — one six-cell fixture with four sections, plus mask, normalization and error variants. All pass in CI. -*Toy data:* built inline in C++ (`BuildAnalyticalDataset`) — a 6-cell ImageGeom, ensemble `CrystalStructures = [999 Unknown, 1 Cubic_High, 0 Hexagonal_High]`, cells chosen to hit identity-cubic, arbitrary cubic, arbitrary hex (two Laue classes), a masked cell, and an invalid-crystal-structure cell. - -*Encoded:* `test/ComputeIPFColorsTest.cpp` — 8 `TEST_CASE`s, all pass in-core and OOC. The main `Class 1/2/3 Oracle (inline analytical dataset)` case carries the Class 2/3/1/4 assertions across four `SECTION`s. - -*Second-engineer review:* **Signed off by Michael Jackson (technical authority), 2026-07-16.** +*Second-engineer review:* The recorded technical-authority sign-off is Michael Jackson, 2026-07-16. ## Bugs found and fixed -No new defect was found during this OOC recertification. Existing defect and deviation dispositions in this report and its sidecar are retained. +All defects in this table are fixed. DREAM3D-NX v7.4.2 contains the fix for `ComputeIPFColorsFilter-D2`. The fix for `ComputeIPFColorsFilter-D3` will be in the DREAM3D-NX release after v7.4.2. + +| Deviation | Defect | Affected released versions | Resolution | +|-----------|--------|----------------------------|------------| +| `ComputeIPFColorsFilter-D2` | Parallel workers incremented a shared non-atomic phase-warning counter, which caused a data race. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | Direct execution uses an atomic counter; Scanline execution counts within its serial loop. | +| `ComputeIPFColorsFilter-D3` | An enabled negative phase index could read before the Crystal Structures array. | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was affected. | Both executors return `-48001` for an enabled negative phase and ignore a masked negative phase. | ## Code path coverage -*17 of 19 code paths exercised. The 2 uncovered paths are the mid-loop cancel branch (requires injecting a cancel signal) and the color-key `default` error `-23510` (unreachable through normal preflight because `ChoicesParameter` already constrains the value to 0–2). Both are low-value.* - -Source: `Algorithms/ComputeIPFColors.cpp` (36 lines), `ComputeIPFColorsDirect.cpp` (276 lines), and `ComputeIPFColorsScanline.cpp` (189 lines). - -Logical phases: (a) `operator()` setup + parallel dispatch, (b) per-cell `convert()` loop, (c) post-loop phase-warning error; plus (d) filter-level preflight / executeImpl wiring. - -| # | Phase | Path | Test case | -|----|--------------|-----------------------------------------------------------------------------------------------|---------------------------------------------------------------------------| -| 1 | (a) Dispatch | mask present & `boolean` → `convert` | `Class 1/2/3 Oracle` (bool `Mask`) | -| 2 | (a) Dispatch | mask present & `uint8` → `convert` | `uint8 mask array drives the black-out path` | -| 3 | (a) Dispatch | mask null (`useMask=false`) → `convert` with null mask | `no-mask path colors every valid cell` | -| 4 | (a) Setup | reference direction normalized before use | `reference direction is normalized` | -| 5 | (b) Per-cell | `shouldCancel()` → early return | *Not directly tested. Requires mid-execution cancel-signal injection; low-value UX guard.* | -| 6 | (b) Per-cell | init each cell to `(0,0,0)` | `Class 1/2/3 Oracle` — masked/invalid cells remain black | -| 7 | (b) Per-cell | `maskArray != null` → `calcIPF = mask[i]` | `Class 1/2/3 Oracle`, `uint8 mask ...` | -| 8 | (b) Per-cell | `phase >= numPhases` → `incrementPhaseWarningCount()` | `phase index out of range returns -48000` | -| 9 | (b) Per-cell | coloring branch: `phase= LaueGroupEnd` (999) → stays black | `Class 1/2/3 Oracle` (cell 4) | -| 12 | (b) Per-cell | not colored — `phase >= numPhases` → stays black | `phase index out of range returns -48000` | -| 13 | (c) Finalize | `m_PhaseWarningCount > 0` → return error `-48000` | `phase index out of range returns -48000` | -| 14 | (c) Finalize | `m_PhaseWarningCount == 0` → return success `{}` | every passing test | -| 15 | (d) Filter | `Color Key` switch 0/1/2 → TSL/PUCM/Nolze-Hielscher | `ColorKey choice reaches algorithm` | -| 16 | (d) Filter | `Color Key` `default` → error `-23510` | *Not directly tested. Unreachable via preflight — `ChoicesParameter` constrains the value to [0,2].* | -| 17 | (d) Filter | `validateNumberOfTuples` fails → error `-651` | `Preflight Error - Cell array tuple count mismatch (-651)` | -| 18 | (d) Filter | preflight creates the 3-component uint8 output array | every passing test (preflight is `REQUIRE`-valid) | -| 19 | Scanline | A 65,536-cell block and one-cell tail, Bool/UInt8 masks | `real HDF5 65536-block color oracle` — exact black, cubic red, and hex green RGB tuples | +19 of 34 paths exercised. The uncovered rows identify gaps in the mapped assertions. + +Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeIPFColors.cpp` (36 lines), `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeIPFColorsDirect.cpp` (276 lines), `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeIPFColorsDirect.hpp` (85 lines), `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeIPFColorsScanline.cpp` (189 lines), and `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeIPFColorsFilter.cpp` (210 lines). + +Test names below omit the common `OrientationAnalysis::ComputeIPFColorsFilter:` prefix. Shared per-cell rows describe the logical operation, not every executor and storage combination. + +| # | Phase | Path | Test case | +|---|-------|------|-----------| +| 1 | Dispatch | Boolean mask selects boolean mask access | `Class 1/2/3 Oracle (inline analytical dataset)` | +| 2 | Dispatch | UInt8 mask selects byte mask access | `uint8 mask array drives the black-out path` | +| 3 | Dispatch | Disabled mask colors valid cells without mask access | `no-mask path colors every valid cell` | +| 4 | Setup | Normalize the reference direction | `reference direction is normalized` | +| 5 | Direct | Stop the cell loop on cancellation | *Not directly tested. Requires cancel-signal injection.* | +| 6 | Per-cell | Initialize each RGB tuple to black | `Class 1/2/3 Oracle (inline analytical dataset)` | +| 7 | Per-cell | Read an enabled mask | `Class 1/2/3 Oracle (inline analytical dataset)`; `uint8 mask array drives the black-out path` | +| 8 | Per-cell | Count a phase at or above the ensemble limit | `phase index out of range returns -48000` | +| 9 | Per-cell | Valid cubic and hexagonal phases call EbsdLib and write RGB | `Class 1/2/3 Oracle (inline analytical dataset)` — four exact reference colors | +| 10 | Per-cell | A masked cell stays black | `Class 1/2/3 Oracle (inline analytical dataset)`; `uint8 mask array drives the black-out path` | +| 11 | Per-cell | An invalid crystal structure stays black | `Class 1/2/3 Oracle (inline analytical dataset)` | +| 12 | Per-cell | A phase above the ensemble limit stays black | *Not directly tested. The upper-bound test checks the error code, not the RGB tuple.* | +| 13 | Finalize | A nonzero phase-warning count returns `-48000` | `phase index out of range returns -48000` | +| 14 | Finalize | Valid inputs return success | `Class 1/2/3 Oracle (inline analytical dataset)` | +| 15 | Filter | Color Key 0 selects TSL | `Class 1/2/3 Oracle (inline analytical dataset)` | +| 16 | Filter | Invalid Color Key reaches fallback error `-23510` | *Not directly tested. ChoicesParameter rejects values outside 0 through 2 before execution.* | +| 17 | Preflight | Different Euler/phase tuple counts return `-651` | `Preflight Error - Cell array tuple count mismatch (-651)` | +| 18 | Preflight | Create a three-component uint8 array with the input tuple count | `Class 1/2/3 Oracle (inline analytical dataset)` | +| 19 | Scanline | Process a 65,536-cell block and a one-cell tail | *Not directly tested. The dedicated `real HDF5 65536-block color oracle` case is hidden from the default CI selection.* | +| 20 | Validation | An enabled negative phase returns `-48001` | `negative Phase bounds` / `Enabled negative Phase returns an error` | +| 21 | Validation | A masked negative phase does not cause an error | `negative Phase bounds` / `Masked negative Phase is ignored` | +| 22 | Filter | Color Key 1 selects PUCM | `ColorKey choice reaches algorithm` — output differs from TSL | +| 23 | Filter | Color Key 2 selects Nolze-Hielscher | `ColorKey choice reaches algorithm` — output differs from TSL | +| 24 | Scanline | Stop before a block on cancellation | *Not directly tested. Requires cancel-signal injection.* | +| 25 | Scanline I/O | Propagate a Crystal Structures read failure | *Not directly tested. Requires an injected store read failure.* | +| 26 | Scanline I/O | Propagate an Euler Angles read failure | *Not directly tested. Requires an injected store read failure.* | +| 27 | Scanline I/O | Propagate a Phases read failure | *Not directly tested. Requires an injected store read failure.* | +| 28 | Scanline I/O | Propagate a boolean mask read failure | *Not directly tested. Requires an injected store read failure.* | +| 29 | Scanline I/O | Propagate a uint8 mask read failure | *Not directly tested. Requires an injected store read failure.* | +| 30 | Scanline I/O | Propagate an RGB write failure | *Not directly tested. Requires an injected store write failure.* | +| 31 | Loop | Zero cells skip the cell or block loop | *Not directly tested. No empty fixture exists.* | +| 32 | Per-cell | An upper-bound phase still causes `-48000` when masked | *Not directly tested. No fixture combines these conditions.* | +| 33 | Preflight | An enabled mask with a different tuple count returns `-651` | *Not directly tested. The tuple-count fixture disables the mask.* | +| 34 | Direct | Multiple workers increment the atomic phase-warning count | *Not directly tested. The error fixture contains only one invalid phase and does not check the count.* | ## Test inventory | Test case | Status | Notes | |-----------|--------|-------| -| `OrientationAnalysis::ComputeIPFColorsFilter: Class 1/2/3 Oracle (inline analytical dataset)` | new-for-V&V | Replaces the retired circular-oracle test. 4 SECTIONs: Class 3 red-corner, Class 2 EbsdLib cross-check over 4 colored cells, Class 1 masked→black, Class 1 invalid-CS→black. | -| `OrientationAnalysis::ComputeIPFColorsFilter: uint8 mask array drives the black-out path` | new-for-V&V | Exercises `convert` dispatch; masked cell black, good cell colored. | -| `OrientationAnalysis::ComputeIPFColorsFilter: no-mask path colors every valid cell` | new-for-V&V | Exercises null-mask `convert`; previously-masked cell now matches EbsdLib reference; invalid-CS cell still black. | -| `OrientationAnalysis::ComputeIPFColorsFilter: reference direction is normalized` | new-for-V&V | `[0,0,5]` output byte-identical to `[0,0,1]`. | -| `OrientationAnalysis::ComputeIPFColorsFilter: phase index out of range returns -48000` | new-for-V&V | Corrupts a phase to an out-of-range ensemble index; asserts execute error `-48000`. | -| `OrientationAnalysis::ComputeIPFColorsFilter: ColorKey choice reaches algorithm` | kept (retargeted) | Was on the retired exemplar; now runs on the inline dataset. Asserts TSL ≠ PUCM ≠ Nolze-Hielscher output. | -| `OrientationAnalysis::ComputeIPFColorsFilter: Preflight Error - Cell array tuple count mismatch (-651)` | kept | Synthetic mismatched tuple counts → `-651`. | -| `OrientationAnalysis::ComputeIPFColorsFilter: SIMPL Backwards Compatibility` | kept | `DYNAMIC_SECTION` over SIMPL 6.4 + 6.5 conversion fixtures; validates UUID + argument conversion. | -| *(retired)* `OrientationAnalysis::ComputeIPFColors` | retired | Circular oracle — compared filter output against the legacy-produced `IPF Colors` array inside `so3_cubic_high_ipf_001.dream3d` ("produced by SIMPL/DREAM3D … our results should match theirs"). Replaced by the analytical oracle above. | -| `real HDF5 65536-block color oracle` | new-for-V&V | Actual HDF5 input/output and independent boundary expectations; 87 assertions across the configured options. | -| `negative Phase bounds` | kept | Existing invalid participating-phase and skipped-mask validation cases. | - -OOC recertification (2026-09-21): serial CTest passed 9/9 in each DREAM3DNX build. `test/ComputeIPFColorsTest.cpp::real HDF5 65536-block color oracle` passed 87 assertions in the OOC binary. The hidden case checks both Bool and UInt8 masks with actual HDF5 inputs and output. A masked cell is black, cell 65,535 is cubic red, and the one-cell tail is hex green under the existing TSL corner convention. Target-only dispatch counters prove Scanline ran on OOC arrays. Existing precision/library and legacy dispositions are retained. Upstream/develop oracle assertions and tolerances remain intact. No fresh legacy binary comparison is claimed. +| `OrientationAnalysis::ComputeIPFColorsFilter: real HDF5 65536-block color oracle` | new-for-V&V | Hidden. Uses 65,537 cells with boolean and uint8 masks; checks black, cubic red and hexagonal green at indices 0, 65,535 and 65,536, plus storage and dispatch assertions. | +| `OrientationAnalysis::ComputeIPFColorsFilter: Class 1/2/3 Oracle (inline analytical dataset)` | new-for-V&V | Four sections check the cubic red corner, four EbsdLib reference colors, masked black and invalid-structure black; also checks output shape. | +| `OrientationAnalysis::ComputeIPFColorsFilter: uint8 mask array drives the black-out path` | new-for-V&V | Checks a black masked cell and a non-black enabled cell with a byte mask. | +| `OrientationAnalysis::ComputeIPFColorsFilter: no-mask path colors every valid cell` | new-for-V&V | Checks an exact EbsdLib color for the formerly masked cell and black for the invalid-structure cell. | +| `OrientationAnalysis::ComputeIPFColorsFilter: reference direction is normalized` | new-for-V&V | Compares all RGB bytes for reference directions [0,0,1] and [0,0,5]. | +| `OrientationAnalysis::ComputeIPFColorsFilter: phase index out of range returns -48000` | new-for-V&V | Sets phase 5 with three ensemble entries; checks execution error `-48000`. | +| `OrientationAnalysis::ComputeIPFColorsFilter: negative Phase bounds` | kept | Checks `-48001` for an enabled negative phase and success for a masked negative phase; covers D3. | +| `OrientationAnalysis::ComputeIPFColorsFilter: ColorKey choice reaches algorithm` | kept | Uses the inline fixture; checks that PUCM and Nolze-Hielscher each differ from TSL, without comparing them with each other. | +| `OrientationAnalysis::ComputeIPFColorsFilter: Preflight Error - Cell array tuple count mismatch (-651)` | kept | Uses 10 Euler tuples and 9 phase tuples; checks preflight error `-651`. | +| `OrientationAnalysis::ComputeIPFColorsFilter: SIMPL Backwards Compatibility` | kept | `SIMPL 6.5 (UUID)` and `SIMPL 6.4 (Filter_Name)` sections check the converted UUID, comments and arguments. | ## Exemplar archive -- **Archive:** None for this filter. The oracle dataset is constructed inline in C++ (`BuildAnalyticalDataset` in `test/ComputeIPFColorsTest.cpp`); no golden `.dream3d` is downloaded or compared, so no provenance sidecar is required. -- **Retired:** `so3_cubic_high_ipf_001.tar.gz` (SHA512 `dfe4598c…dd616b85`) as an oracle for this filter. Its `download_test_data()` entry in `test/CMakeLists.txt` remains because `CreateEnsembleInfoTest` still consumes the archive. +- **Archive:** `so3_cubic_high_ipf_001.tar.gz` — retired as this filter's test oracle; used for the legacy comparison. Current tests construct their inputs in C++. +- **SHA512:** `dfe4598cd4406e8b83f244302dc4fe0d4367527835c5ddd6567fe8d8ab3484d5b10ba24a8bb31db269256ec0b5272daa4340eedb5a8b397755541b32dd616b85` +- **Provenance:** `src/Plugins/OrientationAnalysis/vv/provenance/ComputeIPFColorsFilter.md` -## Deviations from DREAM3D 6.5.171 +## Deviations from DREAM.3D v6.5.171 -Comparison run on `so3_cubic_high_ipf_001` (343,963 single-phase cubic cells, TSL, refDir [001]) — see `vv/comparisons/ComputeIPFColorsFilter/`. +See `vv/deviations/ComputeIPFColorsFilter.md` for the root cause, affected users and recommendation. Only Part 1 entries appear here. -- `ComputeIPFColorsFilter-D1` — 14/343,963 cells (0.004%) differ from a fresh 6.5.171 run by ±1/255 in one channel (SIMPLNX is byte-identical to the stored legacy reference); precision + library quantization jitter — see `vv/deviations/ComputeIPFColorsFilter.md`. +| Deviation | Observed difference | +|-----------|---------------------| +| `ComputeIPFColorsFilter-D1` | Released DREAM3D-NX TSL colors can differ from DREAM.3D v6.5.171 by one level in an 8-bit channel. | diff --git a/src/Plugins/OrientationAnalysis/vv/ComputeKernelAvgMisorientationsFilter.md b/src/Plugins/OrientationAnalysis/vv/ComputeKernelAvgMisorientationsFilter.md index 8ddb464875..bf7b19d68a 100644 --- a/src/Plugins/OrientationAnalysis/vv/ComputeKernelAvgMisorientationsFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/ComputeKernelAvgMisorientationsFilter.md @@ -1,162 +1,153 @@ # V&V Report: ComputeKernelAvgMisorientationsFilter -| | | -|-----------|--------------------------| -| Plugin | OrientationAnalysis | -| SIMPLNX UUID | `61cfc9c1-aa0e-452b-b9ef-d3b9e6268035` | -| SIMPLNX Human Name | Compute Kernel Average Misorientations | -| DREAM3D 6.5.171 equivalent | `FindKernelAvgMisorientations` — `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/FindKernelAvgMisorientations.{h,cpp}` (UUID `88d332c1-cf6c-52d3-a38d-22f6eae19fa6`) | -| Verified commit | ** | -| Status | COMPLETE | -| Sign-off | *Reopened 2026-07-15 for the `use_feature_ids` feature (issue #1613, branch `topic/kam_ignore_feature_ids`). Prior sign-off: Michael Jackson (2026-06-03) — superseded* | +| | | +|--------|--------------| +| Plugin | OrientationAnalysis | +| SIMPLNX UUID | `61cfc9c1-aa0e-452b-b9ef-d3b9e6268035` | +| SIMPLNX Human Name | Compute Kernel Average Misorientations | +| DREAM.3D v6.5.171 equivalent | `FindKernelAvgMisorientations` (OrientationAnalysis plugin, UUID `88d332c1-cf6c-52d3-a38d-22f6eae19fa6`) | +| Verified commit | ** | +| Status | COMPLETE | +| Sign-off | Michael Jackson, 2026-07-16; second engineer: Jared Duffey, 2026-07-28 | ## At a glance -| Aspect | Current state | -|------------------------|--------------------------| -| Algorithm Relationship | **Port with one legacy bug corrected + one NX-only feature addition** — per-voxel kernel averaging; focal-validity gate; divisor always ≥1 (focal self-included). `QuatF`→`QuatD`; `getMisoQuat`→`calculateMisorientation`; iteration order optimized for cache (`col→row→plane` → `plane→row→col`). D2 (legacy l-loop bound typo `KernelSize.z+1` → `KernelSize.x+1`) corrected at port. UUID reassigned. **New this cycle (issue #1613):** `use_feature_ids` BoolParameter (default `true`). `true` = legacy per-grain behavior (neighbor gate `featureIds[point]==featureIds[neighbor]`, behavior-identical to before). `false` = per-voxel KAM: neighbor contributes iff in-bounds AND `featureIds[neighbor]>0` AND `cellPhases[neighbor]==cellPhases[point]`. Focal gate unchanged. No legacy counterpart — see D3. | -| Oracle (confirmed) | **Class 1 (Analytical) primary** — 6 hand-derived data fixtures: uniform single-feature, x-axis gradient, z-axis gradient (3D path), multi-feature multi-voxel with background, **per-voxel multi-feature (`use_feature_ids=false`), and per-voxel two-phase gates**. **Class 4 (Invariant) companion** — non-negativity, cubic max-angle bound (62.8°), uniform-within-feature ⇒ KAM=0, background-voxel ⇒ KAM=0 exactly, **and mode-equivalence (per-grain ≡ per-voxel bit-for-bit on single-feature single-phase data)**. Class 1 oracle uses pure φ1 Bunge ZXZ rotations `(φ1, 0, 0)` so that for cubic symmetry, the misorientation between any two cells equals `|Δφ1|` (the c-axis 4-fold reduction is identity when φ1 differences are ≤45°). | -| Code paths enumerated | 8 paths enumerated, 8 exercised (re-enumerated for the per-mode neighbor gate; the former path 9, the dead `numVoxel==0` fallback, was removed from SIMPLNX by the review-driven cleanup commit `7f9cddc7d` — see below): (1) focal-valid gate → enter kernel; (2) boundary clamp (out-of-bounds j/k/l, now a signed-index comparison with no separate `neighbor<0` guard) → `continue`; (3) per-grain neighbor feature-id match → accumulate; (4) per-grain neighbor feature-id mismatch → skip; (5) per-voxel neighbor `featureId>0` + phase-match → accumulate; (6) per-voxel neighbor `featureId==0` → skip; (7) per-voxel neighbor phase-mismatch → skip; (8) focal-invalid (`featureIds==0 \|\| cellPhases==0`, now reached via `else`) → KAM=0 directly. | -| Tests today | **9 TEST_CASEs / 9 ctest entries**, 100% pass on in-core and out-of-core builds (see full regression sweep, 2026-07-15). 6 Class 1 fixtures + 2 Class 4 invariant tests (Mode-Equivalence + the 3-sub-section Invariants) + 1 SIMPL backwards-compatibility test. **No exemplar archive consumed by this filter.** | -| Exemplar archive | **None — inline-constructed in test source.** The pre-existing main exemplar TEST_CASE (consumed `6_6_stats_test_v2.tar.gz`) was **retired 2026-06-03** because the exemplar `KernelAverageMisorientations` array was a circular oracle (regenerated from pre-EbsdLib-2.4.1 SIMPLNX output, where the precision shift documented as D1 manifests as a non-zero spurious self-misorientation contribution in every focal voxel — see deviations doc). The 6 Class 1 hand-derived data fixtures, plus the Class 4 invariants, cover all 9 re-enumerated active algorithmic paths and replace the retired test. The shared archive `6_6_stats_test_v2.tar.gz` remains downloaded for `AlignSectionsMutualInformation`, `ComputeShapes`, and `ComputeSchmids` tests; only F#5's consumption line was removed. | -| Legacy comparison | **Runtime A/B against DREAM3D 6.5.171 completed (default per-grain path).** Identical synthetic input (12³ cube, 8 features, single cubic phase, kernel `{1,1,1}`) fed through both `PipelineRunner` (6.5.171 `FindKernelAvgMisorientations`) and `nxrunner`; inputs bit-identical (Quats, FeatureIds). KAM delta: **max \|Δ\|=0.0072°, mean \|Δ\|=0.00075°, 0/1728 cells exceed 0.01°, bidirectional** (928 cells legacy>nx, 800 legacy 0, phase > 0), the algorithm iterates over an axis-aligned kernel of user-specified radius `KernelSize = (x, y, z)`, averages the misorientation between the focal cell's orientation quaternion and every same-feature neighbor's quaternion (including the focal cell itself, which contributes a self-misorientation of 0°), and stores the result. Cells with `featureId == 0` or `phase == 0` are treated as background and assigned KAM = 0 directly. - -The filter is the cell-level analog of the feature-level `ComputeFeatureNeighborMisorientationsFilter`. Like that filter, it consumes `Quats` (cell-level avg-orientations) and `CrystalStructures` (per-phase Laue class index), and delegates the actual cubic/hex/etc. symmetry-reduced disorientation calculation to `ebsdlib::LaueOps::calculateMisorientation()`. Unlike the feature-level filter, the kernel iteration ALWAYS visits the focal cell as part of its neighbor list (via the `j=k=l=0` inner iteration), so the per-cell divisor `numVoxel` is always ≥ 1. - -The output is `KernelAverageMisorientations`, a `Float32Array` co-located in the same `AttributeMatrix` as the input `Cell Data` arrays, sized one-tuple-per-cell with one component per tuple, in degrees. - -**`use_feature_ids` feature (issue #1613):** a new `BoolParameter` (default `true`) selects the neighbor-inclusion rule. With `use_feature_ids = true` (the default, behavior-identical to the legacy filter and to every prior release), a kernel neighbor contributes only if it shares the focal cell's `featureId` — the classic per-grain KAM. With `use_feature_ids = false`, the filter computes a **per-voxel** KAM: a neighbor contributes iff it is in-bounds, has `featureId > 0`, and shares the focal cell's phase — feature boundaries are ignored, so the kernel averages across grain boundaries within the same phase. The focal-validity gate (`featureIds[point] > 0 && cellPhases[point] > 0`) is identical in both modes. +`ComputeKernelAvgMisorientationsFilter` averages each valid cell's misorientation to admitted kernel neighbors, including the focal cell, and writes the result in degrees. The default mode admits the same feature; the optional mode admits positive feature IDs in the same phase. Class 1 averages and Class 4 invariants verify both modes; all pass in CI. ## Algorithm Relationship -*Classification:* **Port (with UUID reassignment + name rename + one inherent legacy bug corrected at port time), plus one NX-only feature addition (`use_feature_ids`, issue #1613).** - -*Evidence:* Cross-checked SIMPLNX algorithm against legacy `FindKernelAvgMisorientations.cpp::execute()`. Same per-voxel outer loop, same per-kernel inner loop, same `KAM = totalMisorientation / numVoxel`. On the **default per-grain path (`use_feature_ids = true`)** the neighbor gate is identical: legacy `if(good && m_FeatureIds[point] == m_FeatureIds[neighbor])` (line 292) ≡ SIMPLNX `featureIds[point] == featureIds[neighborIdx]`. Port-time deltas: - -- `QuatF` → `QuatD`; `getMisoQuat` → `calculateMisorientation` (EbsdLib 2.4.1+ `2·atan2(|v|, w)` precision form — see D1). The `float32`→`float64` promotion of the misorientation math is the second component of the precision family observed in the runtime A/B (below). -- `setParallelizationEnabled` removed (now always parallel via `ParallelData3DAlgorithm`). -- Iteration order `col→row→plane` → `plane→row→col` (cache-friendlier for x-fastest-varying storage; mathematically identical since all writes go to the same `point` index). -- D2 corrected at port. -- UUID reassigned; `Find` → `Compute` rename. - -*NX-only feature addition (issue #1613):* the `use_feature_ids` BoolParameter. When `true` (default) the code path is bit-for-bit the pre-feature per-grain algorithm — the feature is opt-in and does not alter the default output. When `false`, the neighbor gate becomes `featureIds[neighborIdx] > 0 && cellPhases[neighborIdx] == cellPhases[point]` (per-voxel, phase-gated, feature-agnostic). This is a **new capability with no DREAM3D 6.5.171 counterpart** — validated by the Class 1 / Class 4 oracle only, never by legacy comparison (see D3). +*Classification:* Minor changes -*Material PRs since baseline:* commits `25959c1f2` through `3e863da2f` (inclusive) on branch `topic/kam_ignore_feature_ids` add the `use_feature_ids` parameter, the per-mode neighbor gate, three unit tests, and the user-doc update, followed by a small follow-up commit (`3e863da2f`) correcting the A/B tallies in this V&V report. No other change to the core algorithm. - -*Review-driven cleanup (2026-07-16, commit `7f9cddc7d`):* a review-flagged, behavior-preserving cleanup pass refactored the kernel boundary-index handling to signed comparisons (the neighbor index is now computed directly from the clamped `zIdx`/`yIdx`/`xIdx`, removing the vacuous unsigned `< 0` checks and the separate `neighbor<0` guard), hoisted `LaueOps` construction out of the per-cell hot path into a worker member built once per run (constructor-time, shared read-only across parallel ranges — same pattern as `ComputeFeatureFaceMisorientationPerTriangleImpl`), tightened cancellation checking from per-plane to per-row granularity, floored the progress-message increment at 1, and removed the dead `numVoxel==0` fallback (folding the invalid-cell reset into an `else` branch, logically equivalent by De Morgan's law since the two branch conditions are complementary for these non-negative `int32` arrays). No algorithmic or numeric change — verified by the unchanged 9-test suite passing on both in-core and out-of-core builds. +*Evidence:* `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/OrientationAnalysisLegacyUUIDMapping.hpp` maps the legacy UUID to this filter; both implementations average admitted kernel neighbors, while SIMPLNX corrects the X bound and adds same-phase admission. ## Oracle -*Confirmed class:* **Class 1 (Analytical) primary, Class 4 (Invariant) companion.** - -### Class 1 (Analytical) - -Class 1 oracle derived by hand for each fixture in terms of `|Δφ1|` between cell pairs, justified by the cubic FZ analysis below. All quaternions in the test fixtures use the helper `QuatFromPhi1Deg(phi1)` which returns the quaternion form of a pure Bunge ZXZ Euler rotation `(phi1, 0, 0)` with `Phi = phi2 = 0`. This collapses to a single rotation about the z-axis. - -**Cubic FZ argument:** For two cells with pure φ1 rotations differing by Δφ1, the disorientation between them in the cubic FZ equals `|Δφ1|` whenever `|Δφ1| ≤ 45°`. The reasoning: the cubic group's 4-fold rotation about the z-axis reduces φ1 differences modulo 90°; for |Δφ1| ≤ 45°, the reduction is the identity operator (no reduction needed). For all 4 Class 1 fixtures, the maximum φ1 difference between any pair is ≤ 30°, well within the 45° bound. Other cubic symmetry operators (3-fold about [111], 2-fold about [110], etc.) only produce smaller-angle equivalents for misorientations not aligned with a pure z-axis rotation; for pure z-rotations of small magnitude, the identity is the global minimum. - -**Per-fixture expected KAM derivation:** - -| Fixture | Geometry | Kernel | Per-cell expected KAM (degrees) | -|--------------------------------------------|----------|-----------|------------------------------------------------------------| -| `Class 1 - Uniform 2D Single Feature` | 3x3x1 | {1,1,0} | All cells = 0.0 (all in-kernel neighbors share orientation) | -| `Class 1 - 1D x-axis Gradient` | 5x1x1 | {1,0,0} | [2.5, 10/3, 10/3, 10/3, 2.5] (see derivation in test comments) | -| `Class 1 - 1D z-axis Gradient (3D path)` | 1x1x3 | {0,0,1} | [5.0, 20/3, 5.0] | -| `Class 1 - Multi-Feature Multi-Voxel + BG` | 6x1x1 | {1,0,0} | [5.0, 5.0, 10.0, 10.0, 0.0, 0.0] | -| `Class 1 - Per-Voxel Mode` (`use_feature_ids=false`) | 6x1x1 | {1,0,0} | [5.0, 20/3, 10.0, 10.0, 0.0, 0.0] | -| `Class 1 - Per-Voxel Mode Two-Phase Gates` (`use_feature_ids=false`) | 5x1x1 | {1,0,0} | [5.0, 5.0, 0.0, 0.0, 0.0] | +*Class:* 1 (Analytical), with 4 (Invariant) companions. -Detailed per-cell hand-derivations are in the test file's TEST_CASE comments and in `vv/provenance/ComputeKernelAvgMisorientationsFilter.md`. +*Applied:* Pure Z rotations give cubic misorientations of `|Δφ1|` for differences at most 45°; hand averages include the focal zero, and invariants check range, background zeros and mode equivalence. -**Per-voxel-mode fixture derivations (issue #1613):** +*Encoded:* `src/Plugins/OrientationAnalysis/test/ComputeKernelAvgMisorientationsTest.cpp` — six `Class 1` fixtures, `Bounded Cache Full-Depth Kernel`, `Class 4 - Invariants` and `Class 4 - Mode Equivalence on Single Feature`. All pass in CI. -*`Class 1 - Per-Voxel Mode`* — the same 6×1×1 layout as the Multi-Feature fixture (featureIds `[1,1,2,2,0,1]`, phases `[1,1,1,1,0,1]`, φ1 `[0,10,0,20,–,30]°`), kernel `{1,0,0}`, run with `use_feature_ids=false`. A neighbor now contributes iff `featureId>0 && phase==focalPhase`, so feature boundaries are crossed but `featureId=0` and phase-mismatched cells are still excluded: -- cell 0: {self=0, x1(F1,P1)=|10−0|} → 10/2 = **5.0** (same as per-grain). -- cell 1: {x0(F1,P1)=|0−10|, self=0, x2(F2,P1)=|0−10|} → 20/3 ≈ **6.667** — the *diagnostic* cell: per-grain gives 5.0 (x2 skipped as different feature); per-voxel includes x2 because it is a valid same-phase cell. This one value proves the mode actually changed behavior. -- cell 2: {x1(F1,P1)=|10−0|, self=0, x3(F2,P1)=|20−0|} → 30/3 = **10.0**. -- cell 3: {x2(F2,P1)=|0−20|, self=0}; x4 excluded (`featureId=0`) → 20/2 = **10.0**. -- cell 4: focal-invalid (`featureId=0, phase=0`) → **0.0** exactly. -- cell 5: {self=0}; x4 excluded (`featureId=0`) → 0/1 = **0.0**. +*Second-engineer review:* Jared Duffey, 2026-07-28. -*`Class 1 - Per-Voxel Mode Two-Phase Gates`* — 5×1×1, every cell its own feature, two cubic phases (ensemble `[999,1,1]`); featureIds `[1,2,3,4,0]`, phases `[1,1,2,1,1]`, φ1 `[0,10,20,30,40]°`, kernel `{1,0,0}`, `use_feature_ids=false`: -- cell 0: {self=0, x1(P1)=|0−10|} → 10/2 = **5.0** (per-grain would give 0.0 — every feature is a single cell). -- cell 1: {x0(P1)=|10−0|, self=0}; x2 skipped (phase 2 ≠ 1) → 10/2 = **5.0**. -- cell 2: {self=0}; x1, x3 skipped (phase 1 ≠ 2) → 0/1 = **0.0**. -- cell 3: {self=0}; x2 skipped (phase), x4 skipped (`featureId=0`) → 0/1 = **0.0**. -- cell 4: focal `featureId=0` (invalid) → **0.0** exactly, even though its phase>0 — proves the focal gate is unchanged in per-voxel mode. +## Bugs found and fixed -### Class 4 (Invariant) +All defects in this table are fixed. The fixes will be in the DREAM3D-NX release after v7.4.2. DREAM3D-NX v7.4.2 contains the fix for `ComputeKernelAvgMisorientationsFilter-D1`. DREAM3D-NX v7.0.0 contains the fix for `ComputeKernelAvgMisorientationsFilter-D2`. -Class 4 invariants asserted in the `Class 4 - Invariants` TEST_CASE across 3 sub-sections: - -1. **Uniform-orientation single-feature → KAM == 0 everywhere.** Asserted on a 3x3x3 uniform-identity-quaternion fixture with kernel `{1,1,1}` (3D path coverage). -2. **Background cell → KAM == 0 exactly.** Asserted on a 3x1x1 fixture with the middle cell flagged as `(featureId=0, phase=0)`. -3. **Range and non-triviality on the x-axis gradient fixture:** (i) `KAM[i] >= 0` for all cells, (ii) `KAM[i] <= 62.8°` (Mackenzie cubic upper bound), (iii) at least one cell has `KAM > 0` (sanity check that the algorithm actually computed something). - -**Mode-equivalence invariant (`Class 4 - Mode Equivalence on Single Feature`):** on single-feature single-phase data, the per-grain gate (`featureId` match) and the per-voxel gate (`featureId>0 && phase match`) admit *exactly the same* neighbor set for every focal cell, so the two modes must produce **bit-for-bit identical** output. Asserted on a 3×3×3 gradient fixture (φ1 = 2x+3y+4z°, max 18° < 45° FZ bound) with kernel `{1,1,1}`: `REQUIRE(kamPerGrain[i] == kamPerVoxel[i])` for every cell, plus a non-triviality guard that at least one cell is non-zero. This is a derived property (not a hand-computed value), so it holds regardless of the EbsdLib precision class and pins the invariant "the feature is opt-in and does not change output where the two gates coincide." - -The Class 4 invariants are oracle-agnostic — they hold for any input, so they catch regressions even if specific Class 1 expected values were edited away. - -### Class 2, 3, 5 - -N/A — no reference-library invocation (Class 2), no published-paper figure reproduction (Class 3), no expert-visual sign-off (Class 5) needed. Class 1 + Class 4 are sufficient. - -### Second-engineer oracle review - -Recommended pending another engineer review. The multi-feature multi-voxel fixture in particular has 6 hand-derived per-cell expected values; a second pair of eyes on the symmetry-reduced cubic misorientation reasoning would catch arithmetic mistakes in the test comments before the V&V cycle is closed. +| Deviation | Defect | Affected released versions | Resolution | +|-----------|--------|----------------------------|------------| +| `ComputeKernelAvgMisorientationsFilter-D1` | Cubic misorientation loses precision near zero and can inflate the kernel average. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | EbsdLib calculates the reduced angle with explicit quaternion vector components and `2*atan2`. | +| `ComputeKernelAvgMisorientationsFilter-D2` | The legacy X loop uses the Z radius as its upper bound and changes asymmetric kernels. | DREAM.3D v6.5.171 was affected. DREAM3D-NX v7.0.0 through v7.4.2 were not affected. | Each axis uses its own radius and clamps the kernel to the image. | +| `ComputeKernelAvgMisorientationsFilter-D4` | A participating phase or Laue index can access outside its array. | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was affected. | The filter rejects an invalid phase with `-67203` and an invalid Laue index with `-67204` before either executor runs. | +| `ComputeKernelAvgMisorientationsFilter-D5` | A radius of `INT32_MAX` overflows the signed loop upper bound. | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was affected. | The filter clamps unsigned neighbor bounds to the image before iteration. | ## Code path coverage -Re-enumerated for the per-mode neighbor gate (`ComputeKernelAvgMisorientations.cpp:124`). Paths 3–4 are the per-grain branch (`use_feature_ids = true`); paths 5–7 are the per-voxel branch (`use_feature_ids = false`). **Updated 2026-07-16 (commit `7f9cddc7d`):** the former path 9 (`numVoxel==0` fallback) was removed as unreachable dead code — see path 8's note. Path 2's boundary clamp is now a signed-index comparison (`zIdx`/`yIdx`/`xIdx` at `.cpp:100,107,114`) with the neighbor index computed directly from the clamped indices (`.cpp:120`); the separate `neighbor < 0` guard no longer exists because it is now provably unreachable by construction rather than checked at runtime. - -| Path | Description | Exercised by | -|------|------------------------------------------------------------------------------|--------------| -| 1 | Focal-valid gate (`featureIds[point] > 0 && cellPhases[point] > 0`) → enter kernel | All 6 Class 1 fixtures; both Class 4 tests | -| 2 | Kernel cell out-of-bounds (signed boundary clamp on `zIdx`/`yIdx`/`xIdx`) → `continue` | `Class 1 - 1D x-axis Gradient` cells 0/4 (x-boundary); `Class 1 - 1D z-axis Gradient` planes 0/2 (z-boundary); `Class 1 - Uniform 2D` corners; both 3×3×3 fixtures (all faces) | -| 3 | **Per-grain** (`use_feature_ids=true`): in-bounds neighbor + `featureId` match → accumulate miso + numVoxel++ | All default-mode Class 1 fixtures; Class 4 Invariants (i)/(iii); Class 4 Mode-Equivalence (per-grain run) | -| 4 | **Per-grain**: in-bounds neighbor + `featureId` mismatch → skip (no accumulate) | `Class 1 - Multi-Feature Multi-Voxel + Background` (cells 1, 2 see different-feature in-bounds neighbors) | -| 5 | **Per-voxel** (`use_feature_ids=false`): in-bounds neighbor + `featureId>0` + phase match → accumulate | `Class 1 - Per-Voxel Mode` cells 1, 2 (cross-feature same-phase include); `Class 1 - Per-Voxel Two-Phase Gates` cells 0, 1; Class 4 Mode-Equivalence (per-voxel run) | -| 6 | **Per-voxel**: neighbor `featureId == 0` → skip | `Class 1 - Per-Voxel Mode` cells 3, 5 (x=4 background excluded); `Class 1 - Per-Voxel Two-Phase Gates` cell 3 (x=4 excluded) | -| 7 | **Per-voxel**: neighbor phase mismatch (`cellPhases[neighbor] != cellPhases[point]`) → skip | `Class 1 - Per-Voxel Two-Phase Gates` cell 2 (excludes phase-1 x=1/x=3), cells 1 & 3 (exclude phase-2 x=2) | -| 8 | Focal-invalid (`featureIds == 0 \|\| cellPhases == 0`, reached via `else` since commit `7f9cddc7d`) → KAM = 0 directly. The former path 9 (`numVoxel == 0` fallback) was removed by this commit: in both modes the focal voxel always self-contributes when the focal is valid (per-grain: `featureId==featureId`; per-voxel: focal-valid ⇒ `featureId>0` and `phase==phase`), guaranteeing numVoxel ≥ 1 whenever path 1 is entered, so the fallback was unreachable by construction. | `Class 1 - Multi-Feature` cell 4; `Class 1 - Per-Voxel Mode` cell 4; `Class 1 - Per-Voxel Two-Phase Gates` cell 4 (`featureId=0` yet phase>0 — proves focal gate unchanged); Class 4 Invariants (ii) | - -8 of 8 paths exercised by the V&V suite. The former path 9 (`numVoxel==0` fallback, previously "unreachable by construction") was removed from SIMPLNX by the review-driven cleanup commit `7f9cddc7d` (2026-07-16); it was purely dead code and its removal is behavior-preserving, confirmed by the unchanged 9-test suite passing on both in-core and out-of-core builds. +29 of 46 paths exercised. Each uncovered row states the missing fixture or the guard that prevents direct execution. + +Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeKernelAvgMisorientations.cpp` (92 lines), `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeKernelAvgMisorientationsDirect.cpp` (251 lines), `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeKernelAvgMisorientationsScanline.cpp` (964 lines), and `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeKernelAvgMisorientationsFilter.cpp` (181 lines). + +Test names below omit the `OrientationAnalysis::ComputeKernelAvgMisorientationsFilter:` or `OrientationAnalysis::ComputeKernelAvgMisorientations:` prefix. Rows 1–8 describe Direct and rolling-window calculation; the bounded-cache gaps have separate rows. + +| # | Phase | Path | Test case | +|---|-------|------|-----------| +| 1 | Calculate | Positive focal feature and phase enter the kernel | `Class 1 - Uniform 2D Single Feature` | +| 2 | Calculate | Clamp X, Y and Z neighbor bounds to the image | `Class 1 - Uniform 2D Single Feature`; `Class 1 - 1D x-axis Gradient`; `Class 1 - 1D z-axis Gradient (3D path)` | +| 3 | Calculate | Same-feature mode admits a matching feature | `Class 1 - Multi-Feature Multi-Voxel + Background` | +| 4 | Calculate | Same-feature mode rejects a different feature | `Class 1 - Multi-Feature Multi-Voxel + Background` | +| 5 | Calculate | Same-phase mode admits positive features across a feature boundary | `Class 1 - Per-Voxel Mode (use_feature_ids = false)` | +| 6 | Calculate | Same-phase mode rejects feature zero | `Class 1 - Per-Voxel Mode (use_feature_ids = false)` | +| 7 | Calculate | Same-phase mode rejects a different phase | `Class 1 - Per-Voxel Mode Two-Phase Gates` | +| 8 | Calculate | Focal feature zero writes zero, including with a positive phase | `Class 1 - Multi-Feature Multi-Voxel + Background`; `Class 1 - Per-Voxel Mode Two-Phase Gates` | +| 9 | Validation | Participating phase is outside Crystal Structures: error `-67203` | `Phase and Laue Index Bounds` / `Participating Phase returns an error` | +| 10 | Validation | Participating Laue index is outside the operator list: error `-67204` | `Phase and Laue Index Bounds` / `Participating Laue index returns an error` | +| 11 | Validation | Ignore an invalid phase on feature zero | `Phase and Laue Index Bounds` / `Phase on a background Feature is ignored` | +| 12 | Calculate | Positive focal feature has a nonpositive phase | *Not directly tested. Background fixtures also set the feature to zero.* | +| 13 | Calculate | Negative feature ID follows the background rule | *Not directly tested. Fixtures use zero or positive feature IDs.* | +| 14 | Dispatch | Direct calculation on arrays in memory | `Class 1 - 1D x-axis Gradient` — algorithm scenario harness | +| 15 | Dispatch | Scanline rolling-window calculation on arrays in memory | `Class 1 - 1D z-axis Gradient (3D path)` — algorithm scenario harness | +| 16 | Dispatch | Bounded-cache calculation in same-feature mode | `Bounded Cache Full-Depth Kernel` — all values match `[10,7,6,7,10]` by Z slice | +| 17 | Calculate | Bounded-cache calculation in same-phase mode | *Not directly tested. The forced bounded-cache fixture enables same-feature mode.* | +| 18 | Calculate | Bounded cache skips background cells or different-feature neighbors | *Not directly tested. The forced bounded-cache fixture has one positive feature and phase.* | +| 19 | Calculate | Maximum signed radius clamps to the image | `Scanline Handles Maximum Kernel Radius` — Direct and Scanline scenarios | +| 20 | Setup | An empty image returns an empty output | `Scanline Handles Empty Image Geometry` — each axis is zero in turn | +| 21 | Plan | Kernel vector has fewer or more than three values: error `-67202` | `Working Set Input Validation` / `short kernel is rejected before indexing`; `oversized kernel is rejected` | +| 22 | Plan | Negative kernel radius: error `-67202` | `Working Set Input Validation` / `negative kernel radius is rejected as invalid input` | +| 23 | Plan | Live cache use reduces the memory cap | `Working Set Plan` / `live cache use reduces the algorithm cap` | +| 24 | Plan | Rolling window fits the cap, including zero dimensions | `Working Set Plan` / `normal kernel selects rolling window`; `Working Set Budget Boundaries` / `zero dimensions have an empty rolling working set` | +| 25 | Plan | Large window selects bounded blocks and shrinks row-aligned blocks | `Working Set Plan` / `large window selects bounded blocks` | +| 26 | Plan | Zero, exhausted or sub-tuple budget keeps one block and slot | `Working Set Budget Boundaries` | +| 27 | Plan | Slice tuple count overflows: error `-67200` | `Working Set Plan` / `slice tuple overflow reports complete context` | +| 28 | Plan | Total tuple or quaternion component count overflows: error `-67200` | `Working Set Overflow Boundaries` / `total tuple count overflow`; `total quaternion value count overflow` | +| 29 | Plan | Rolling input, output or total bytes overflow: error `-67200` | `Working Set Overflow Boundaries` — three byte-count sections | +| 30 | Plan | Other checked window, block and cache-size arithmetic rejects overflow | *Not directly tested. Earlier tuple and byte checks and bounded radii/block sizes constrain these quantities.* | +| 31 | Cache | Small fully associative cache loads, reuses and replaces blocks; writes a partial final block | `Bounded Cache Full-Depth Kernel` — eight slots, 1,280 tuples per block and 4,096 tuples per slice | +| 32 | Cache | More than 16 slots select a set-associative cache | *Not directly tested. The forced bounded-cache fixture has eight slots.* | +| 33 | Cancel | Direct stops within a row; Scanline stops before a plane or block | *Not directly tested. No fixture injects cancellation.* | +| 34 | I/O | Validation propagates Crystal Structures, FeatureIds or Phases read errors | *Not directly tested. Requires injected store read failures.* | +| 35 | I/O | Rolling execution propagates ensemble or input-slice read errors | *Not directly tested. Requires injected store read failures.* | +| 36 | I/O | Rolling execution propagates output-slice write errors | *Not directly tested. Requires an injected store write failure.* | +| 37 | I/O | Bounded execution propagates ensemble, focal or neighbor block read errors | *Not directly tested. Requires injected store read failures.* | +| 38 | I/O | Bounded execution propagates output-block write errors | *Not directly tested. Requires an injected store write failure.* | +| 39 | Calculate | Scanline zero-neighbor fallback writes zero | *Not directly tested. A valid focal cell always includes itself.* | +| 40 | Validation | Read more than one 65,536-tuple validation chunk | *Not directly tested. The largest execution fixture has 20,480 cells.* | +| 41 | Calculate | Calculate misorientation for a non-cubic Laue class | *Not directly tested. All analytical orientations use cubic symmetry.* | +| 42 | Preflight | Create one float32 output component with the phase array tuple shape | `Class 1 - Uniform 2D Single Feature` — execution and tuple-dimension check | +| 43 | Conversion | Convert legacy paths, output name and UUID | `SIMPL Backwards Compatibility` — both dynamic sections | +| 44 | Setup | Internal rolling and bounded executors return early on empty dimensions | *Not directly tested. The Scanline entry point returns before either executor is called.* | +| 45 | Cache | Rolling window replaces a slice slot after its first full window | *Not directly tested. Rolling fixtures have at most three Z slices; the five-slice fixture forces bounded-cache execution.* | +| 46 | Preflight | Feature IDs, Phases or Quaternions tuple count differs from the selected geometry cell count: error `-67205` | `Geometry cell tuple counts` — all inputs short or long, and each input short in turn, in both feature-ID modes | ## Test inventory -| TEST_CASE | Category | Status | ctest entry | -|--------|----------|--------|--------| -| `: SIMPL Backwards Compatibility` | Compat | kept | Yes (2 dynamic sections: 6.4 + 6.5) | -| `: Class 1 - Uniform 2D Single Feature` | Class 1 | kept | Yes | -| `: Class 1 - 1D x-axis Gradient` | Class 1 | kept | Yes | -| `: Class 1 - 1D z-axis Gradient (3D path)` | Class 1 | kept | Yes | -| `: Class 1 - Multi-Feature Multi-Voxel + Background` | Class 1 | kept | Yes | -| `: Class 1 - Per-Voxel Mode (use_feature_ids = false)` | Class 1 | **new (#1613)** | Yes — per-voxel cross-feature include + `featureId=0` exclude; expected `{5.0, 20/3, 10.0, 10.0, 0, 0}` | -| `: Class 1 - Per-Voxel Mode Two-Phase Gates` | Class 1 | **new (#1613)** | Yes — per-voxel phase-mismatch exclude + `featureId=0` focal gate; expected `{5.0, 5.0, 0, 0, 0}` | -| `: Class 4 - Mode Equivalence on Single Feature` | Class 4 | **new (#1613)** | Yes — per-grain ≡ per-voxel bit-for-bit on single-feature single-phase 3×3×3 | -| `: Class 4 - Invariants` (3 sub-sections) | Class 4 | kept | Yes (3 SECTIONs) | -| ~~`: ComputeKernelAvgMisorientationsFilter` (legacy exemplar test)~~ | RETIRED | — | Retired 2026-06-03 (circular oracle from pre-EbsdLib-2.4.1 SIMPLNX output) | - -9 active TEST_CASEs / 9 ctest entries (confirmed via `ctest -N -R "ComputeKernelAvgMisorientations"`); 100% pass on both in-core and out-of-core builds (full regression sweep, 2026-07-15). +All pass in CI. + +| Test case | Status | Notes | +|-----------|--------|-------| +| `OrientationAnalysis::ComputeKernelAvgMisorientations: Working Set Plan` | kept | Checks rolling-window size, live-budget reduction, row-aligned fallback blocks and slice-overflow diagnostics. | +| `OrientationAnalysis::ComputeKernelAvgMisorientations: Working Set Input Validation` | kept | Checks short and oversized vectors, a negative radius, and acceptance of `INT32_MAX` radii by the planner. | +| `OrientationAnalysis::ComputeKernelAvgMisorientations: Working Set Budget Boundaries` | kept | Checks zero, exhausted and sub-tuple budgets, plus zero dimensions. | +| `OrientationAnalysis::ComputeKernelAvgMisorientations: Working Set Overflow Boundaries` | kept | Checks total tuples, quaternion values and three byte-count overflow boundaries; checks full diagnostic text. | +| `OrientationAnalysis::ComputeKernelAvgMisorientationsFilter: Scanline Handles Maximum Kernel Radius` | kept | Runs a one-cell image with all radii at `INT32_MAX`; requires one zero output value. Covers D5. | +| `OrientationAnalysis::ComputeKernelAvgMisorientationsFilter: Scanline Handles Empty Image Geometry` | kept | Checks zero output tuples in dynamic sections `dimensions = 0 x 1 x 1`, `dimensions = 1 x 0 x 1` and `dimensions = 1 x 1 x 0`. | +| `OrientationAnalysis::ComputeKernelAvgMisorientationsFilter: SIMPL Backwards Compatibility` | kept | Checks UUID, comments, paths and output name in `SIMPL 6.5 (UUID)` and `SIMPL 6.4 (Filter_Name)` dynamic sections. | +| `OrientationAnalysis::ComputeKernelAvgMisorientationsFilter: Class 1 - Uniform 2D Single Feature` | new-for-V&V | Checks nine zero values on a 3×3×1 image within 0.0001° and checks tuple dimensions. | +| `OrientationAnalysis::ComputeKernelAvgMisorientationsFilter: Phase and Laue Index Bounds` | kept | Checks errors `-67203` and `-67204`; accepts an invalid phase on background feature zero. Covers D4. | +| `OrientationAnalysis::ComputeKernelAvgMisorientationsFilter: Class 1 - 1D x-axis Gradient` | new-for-V&V | Checks all five values against `[2.5,10/3,10/3,10/3,2.5]` within 0.001°. | +| `OrientationAnalysis::ComputeKernelAvgMisorientationsFilter: Class 1 - 1D z-axis Gradient (3D path)` | new-for-V&V | Checks all three values against `[5,20/3,5]` within 0.001°. | +| `OrientationAnalysis::ComputeKernelAvgMisorientationsFilter: Class 1 - Multi-Feature Multi-Voxel + Background` | new-for-V&V | Checks `[5,5,10,10,0,0]`, including an exact background zero and an isolated focal cell. | +| `OrientationAnalysis::ComputeKernelAvgMisorientationsFilter: Class 1 - Per-Voxel Mode (use_feature_ids = false)` | new-for-V&V | Checks `[5,20/3,10,10,0,0]`; the changed second value proves that the mode admits a different feature. | +| `OrientationAnalysis::ComputeKernelAvgMisorientationsFilter: Class 1 - Per-Voxel Mode Two-Phase Gates` | new-for-V&V | Checks `[5,5,0,0,0]` for two cubic phases and an invalid focal feature. | +| `OrientationAnalysis::ComputeKernelAvgMisorientationsFilter: Class 4 - Mode Equivalence on Single Feature` | new-for-V&V | Compares all 27 values bit-for-bit between modes on a single-feature, single-phase gradient; requires nonzero output. | +| `OrientationAnalysis::ComputeKernelAvgMisorientationsFilter: Class 4 - Invariants` | new-for-V&V | Checks uniform zero, exact background zero, nonnegativity, a 62.8° cubic bound and nonzero gradient output in three sections. | +| `OrientationAnalysis::ComputeKernelAvgMisorientationsFilter: Bounded Cache Full-Depth Kernel` | kept | Checks all 20,480 values against slice values `[10,7,6,7,10]` within 0.001° under a 1 MiB budget. | +| `OrientationAnalysis::ComputeKernelAvgMisorientationsFilter: Geometry cell tuple counts` | new-for-V&V | Checks error `-67205` and diagnostic context when all cell inputs are short or long, or each input is short in turn. Accepts matching tuple counts outside Cell Data in both feature-ID modes. | ## Exemplar archive -**None** — inline-constructed. The pre-V&V test (now retired) consumed `6_6_stats_test_v2.tar.gz` (SHA512 `e84999...089723`, downloaded from the BlueQuartz Data_Archive release). The archive contains exemplar `KernelAverageMisorientations` arrays generated from a pre-2.4.1 SIMPLNX build, which embeds the spurious self-misorientation precision noise described in D1. Comparing the post-2.4.1 SIMPLNX output against those exemplars fails by ~0.01-0.05° per cell on real Small_IN100 data. The exemplar arrays cannot be re-generated against the post-2.4.1 build (circular oracle pattern), so the test was retired and replaced with the analytical / invariant suite above. - -The shared archive remains referenced in `src/Plugins/OrientationAnalysis/test/CMakeLists.txt` (line 130) for use by `AlignSectionsMutualInformation`, `ComputeShapes`, and `ComputeSchmids` tests. Only F#5's consumption line was removed. - -- **Provenance:** `vv/provenance/ComputeKernelAvgMisorientationsFilter.md` — the canonical record of how the inlined data fixtures were designed and how the expected values were derived. +- **Archive:** `6_6_stats_test_v2.tar.gz` — retired for this filter; current fixtures are inline. +- **SHA512:** `e84999dec914d81efce4fc4237c49c9bf32e48381b1e79f58aa4df934f0d7606cd7a948f9a5e7b17a126a7944cc531b531cfdc70756ca3e2207b20734e089723` +- **Provenance:** `src/Plugins/OrientationAnalysis/vv/provenance/ComputeKernelAvgMisorientationsFilter.md` -## Deviations from DREAM3D 6.5.171 +## Deviations from DREAM.3D v6.5.171 -See `vv/deviations/ComputeKernelAvgMisorientationsFilter.md` for the canonical, ID-stable list: +See `vv/deviations/ComputeKernelAvgMisorientationsFilter.md` for the root cause, affected users and recommendation for each Part 1 entry. -- **`ComputeKernelAvgMisorientationsFilter-D1`** — EbsdLib 2.4.1 CubicOps precision improvement (non-deviation in the algorithmic sense; precision class). The pre-2.4.1 `acos(w near 1)` form produces a spurious ~0.03° angle on float32-sourced identical quaternions, which inflates every per-cell self-misorientation contribution to the KAM. The 2.4.1 `2*atan2(|v|, w)` form returns 0° as expected. **Confirmed by runtime A/B this cycle:** on identical 12³ synthetic input (default per-grain path, kernel `{1,1,1}`), max \|Δ\|=0.0072°, mean \|Δ\|=0.00075°, 0/1728 cells > 0.01°, gating provably identical — the delta is purely `calculateMisorientation` precision (D1 + the `QuatF`→`QuatD` port delta). -- **`ComputeKernelAvgMisorientationsFilter-D2`** — Legacy `FindKernelAvgMisorientations.cpp:264` uses `KernelSize.z + 1` as the upper bound of the x-direction inner loop (should be `KernelSize.x + 1`). SIMPLNX has the correct form. Dormant when `KernelSize.x == KernelSize.z`; fires for asymmetric kernels. Logged in `bug_triage.md`. -- **`ComputeKernelAvgMisorientationsFilter-D3`** — `use_feature_ids = false` (per-voxel KAM) is an **NX-only capability** added for issue #1613. DREAM3D 6.5.171 `FindKernelAvgMisorientations` has no equivalent (it is per-grain only), so there is nothing to A/B against; the mode is validated by the Class 1 per-voxel fixtures and the Class 4 mode-equivalence invariant. The default (`use_feature_ids = true`) is unchanged and remains legacy-comparable. +| Deviation | Observed difference | +|-----------|---------------------| +| `ComputeKernelAvgMisorientationsFilter-D1` | Cubic KAM values can differ because of the corrected angle calculation and double-precision quaternion arithmetic. | +| `ComputeKernelAvgMisorientationsFilter-D2` | SIMPLNX uses the requested X radius on both sides of an asymmetric kernel. DREAM.3D v6.5.171 uses the Z radius on the positive X side. | +| `ComputeKernelAvgMisorientationsFilter-D3` | `use_feature_ids=false` admits same-phase neighbors across feature boundaries. DREAM.3D v6.5.171 has no equivalent mode. | diff --git a/src/Plugins/OrientationAnalysis/vv/ComputeSchmidsFilter.md b/src/Plugins/OrientationAnalysis/vv/ComputeSchmidsFilter.md index b373aa326d..84f23dd84a 100644 --- a/src/Plugins/OrientationAnalysis/vv/ComputeSchmidsFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/ComputeSchmidsFilter.md @@ -1,120 +1,67 @@ # V&V Report: ComputeSchmidsFilter -| | | -|-----------|--------------------------| -| Plugin | OrientationAnalysis | +| | | +|--------|--------------| +| Plugin | OrientationAnalysis | | SIMPLNX UUID | `b4681855-0a3d-4237-97f2-5aec509115c4` | -| DREAM3D 6.5.171 equivalent | `FindSchmids` (SIMPL UUID `e67ca06a-176f-58fc-a676-d6ee5553511a`) - `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/FindSchmids.{h,cpp}` | +| SIMPLNX Human Name | Compute Schmid Factors | +| DREAM.3D v6.5.171 equivalent | `FindSchmids` (SIMPL UUID `e67ca06a-176f-58fc-a676-d6ee5553511a`) | | Verified commit | ** | -| Status | COMPLETE | -| Sign-off | *Michael Jackson * | +| Status | READY FOR REVIEW | +| Sign-off | Michael Jackson, 2026-08-21; second engineer: pending | ## At a glance -| Aspect | Current state | -|------------------------|--------------------------| -| Algorithm Relationship | Port with with bug fixes. See 'Dependency' section | -| Oracle (confirmed) | Class 1 (Analytical) - **12 value-asserting fixtures**: 5 auto-path loading directions, 4 exactly-representable quaternions, and 3 override-path cases. Class 4 assertions cover the physical bound `0 <= m <= 0.5`, angle-component relationships, fixed-point `Poles`, scale invariance, and cubic-symmetry invariance. The fixtures are inline and independent of archived filter output. | -| Code paths enumerated | 21 of 22 paths exercised - only the cancel path is untested. | -| Tests today | 6 registered target cases plus one hidden HDF5 boundary test. Paired target selections pass 6/6. | -| Exemplar archive | **None for this filter.** The circular `6_6_stats_test_v2.tar.gz` comparison was retired. | -| Legacy comparison | **Run — SIMPLNX vs DREAM3D 6.5.171** One shared six-Feature input was exercised through 10 original pipeline pairs plus 3 zero-vector validation pairs. The independent rerun confirmed **291/291 expected relationships**: auto-path floats matched the exact D1 bias factors, auto-path `SlipSystems` and `Poles` were bit-identical, override-path floats agreed within `1.2e-7` relative, and D5 was traced to equivalent symmetry tables with different ordering and tied maxima. 6 deviations: **D1** truncated cubic normalizers; **D2** legacy `-301` skipped-row sentinel; **D3** undefined library outputs; **D4** mode-dependent angle units, names, and error reporting; **D5** table-relative override index; **D6** malformed-input validation. See the deviations document for root-cause detail. | -| Bug flags | **Four bugs resolved in SIMPLNX or EbsdLib:** `ComputeSchmidsFilter-D1` (truncated cubic normalizers), `ComputeSchmidsFilter-D2` (legacy `-301` skipped-row initialization), `ComputeSchmidsFilter-D3` (undefined orientation-library outputs), and `ComputeSchmidsFilter-D6` (missing malformed-input validation). | -| V&V phase | Historical status and sign-off retained. Section 4.3 recertification adds real-HDF5 analytical boundary evidence and paired runtime checks. | +| Aspect | Current state | +|--------|---------------| +| Algorithm Relationship | Minor changes to `FindSchmids` in DREAM.3D v6.5.171. SIMPLNX retains the feature calculation with higher precision, input checks and corrected library outputs. | +| Oracle (confirmed) | Classes 1 and 4 use 12 analytical fixtures in `test/ComputeSchmidsTest.cpp`. All pass in CI. | +| Code paths enumerated | 20 of 25 exercised. Cancellation, the hidden storage fixture, the sentinel-only loop, other Laue classes and one option combination remain outside this count. | +| Tests today | 7 test cases: 3 analytical, 2 option/input-validation and 1 conversion case, plus 1 hidden storage case. The six ordinary cases run in CI; all pass in CI. | +| Exemplar archive | `6_6_stats_test_v2.tar.gz` is retired for this filter. Current tests construct their inputs and use analytical expectations. | +| Legacy comparison | Not run for the current algorithm. The DREAM.3D v6.5.171 comparison dated 2026-08-20 precedes the latest algorithm change. | +| Bug flags | None | +| V&V phase | COMPLETE | ## Summary -`ComputeSchmidsFilter` computes, for each Feature, the largest Schmid factor over the slip systems of that Feature's crystal structure, given a sample-frame loading direction and the Feature's average orientation. Verification uses inline Class 1 analytical fixtures plus Class 4 invariants; the circular production-scale exemplar was retired. The cycle found the original four defects plus two input-validation gaps during adversarial review and documents six deviations from DREAM3D 6.5.171. - -## Dependency state - -- EbsdLib version 3.1.1 +`ComputeSchmidsFilter` calculates each feature's maximum Schmid factor from its average orientation, crystal structure and loading direction. Twelve Class 1 fixtures and Class 4 invariants check the cubic calculations, with additional input and output checks. All pass in CI. ## Algorithm Relationship -**Port with minor algorithm improvements and updates for the SIMPLNX API.** - -*Evidence:* `ComputeSchmids::operator()` preserves the legacy `FindSchmids::execute()` feature iteration, loading-direction transformation, crystal-structure dispatch, and automatic/override slip-system calculations. - -Port-time updates: +*Classification:* Minor changes -1. **Higher-precision calculations.** Legacy holds the orientation matrix, crystal-frame loading direction, Schmid factor, and angle components in `float`; SIMPLNX computes them in `double` and narrows only when storing the `float32` outputs. This improves numerical accuracy without changing the selected slip system. -2. **SIMPLNX API updates and input validation.** Selection parameters validate the required arrays, output actions declare every created array, and preflight/execute guards reject mismatched tuple counts, empty arrays, invalid phase indices, and zero-length input vectors before unsafe access or normalization. -3. **Improved error messages.** SIMPLNX identifies the offending array paths, tuple counts, vector components, phase values, and valid ranges so users can correct invalid inputs directly. - -**Material PRs since baseline:** none identified for this filter. +*Evidence:* `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/OrientationAnalysisLegacyUUIDMapping.hpp` maps `FindSchmids` to this filter; both sources use the same feature loop and automatic/override calculations, with higher precision and added validation in SIMPLNX. ## Oracle -**Class 1 (Analytical)** with **Class 4 (Invariant)** support, and Class 3 as corroboration only. - -The oracle was derived by transcribing three sources by hand — `ComputeSchmids.cpp`, EbsdLib's `Quaternion::toOrientationMatrix()`, and both `CubicOps::getSchmidFactorAndSS` overloads — into closed-form expressions, evaluating them exactly, and writing the results into the test as inline expected values with the derivation in a comment above each. The derivation is reproduced independently at 80 significant digits by `ww_work/ComputeSchmids/oracle.py`, which parses EbsdLib's own 24-element symmetry table out of the source rather than re-typing it. - -Identity quaternion, `Cubic_High`, auto slip-system path: - -| Loading | m | cos phi (Phis) | cos lambda (Lambdas) | SlipSystems | Poles | -|---|---|---|---|---|---| -| `[0,0,1]` | `1/sqrt(6)` = 0.4082482904638630 | `1/sqrt(3)` = 0.5773502691896258 | `1/sqrt(2)` = 0.7071067811865476 | 0 | (0, 0, 100) | -| `[1,1,1]` | `2/(3 sqrt(6))` = 0.2721655269759087 | `1/3` = 0.3333333333333333 | `2/sqrt(6)` = 0.8164965809277260 | 4 | (57, 57, 57) | -| `[0,1,1]` | `1/sqrt(6)` = 0.4082482904638630 | `2/sqrt(6)` = 0.8164965809277260 | `1/2` = 0.5 | 1 | (0, 70, 70) | -| `[1,2,3]` | `8/sqrt(294)` = 0.4665694748158435 | `4/sqrt(42)` = 0.6172133998483701 | `4/sqrt(28)` = 0.7559289460184544 | 10 | (26, 53, 80) | - -`[0,0,1]` and `[1,1,1]` and `[0,1,1]` all carry ties at the maximum and pin the strict-`>` tie-break to the lowest index. `[1,2,3]` has a unique maximum and pins the enumeration order. `[0,1,1]` is the row that separates `Phis` from `Lambdas` (0.8165 versus 0.5) — on the other three rows a swapped pair would go unnoticed. `[3,6,9]` repeats `[1,2,3]` to check that the loading direction is normalized. - -Rotated quaternions, loading `[1,2,3]`. Each is exactly representable in `float32` and produces an orientation matrix whose entries are all `0` or `+/-1`, so there is no floating-point slack in the expected `Poles`. All four are cubic symmetry operations, so `m` and both angle components must be identical across them while `SlipSystems` and `Poles` move — a Class 4 symmetry invariance riding on the Class 1 values: - -| Quaternion (x,y,z,w) | Orientation matrix | crystalLoading | SlipSystems | Poles | -|---|---|---|---|---| -| `(0,0,0,1)` identity | `I` | `(1,2,3)/sqrt(14)` | 10 | (26, 53, 80) | -| `(0,0,1,0)` 180 deg about Z | `diag(-1,-1,1)` | `(-1,-2,3)/sqrt(14)` | 8 | (-26, -53, 80) | -| `(0.5,0.5,0.5,0.5)` 120 deg about [111] | `[[0,0,1],[1,0,0],[0,1,0]]` | `(3,1,2)/sqrt(14)` | 6 | (80, 26, 53) | -| `(1,0,0,0)` 180 deg about X | `diag(1,-1,-1)` | `(1,-2,-3)/sqrt(14)` | 1 | (26, -53, -80) | - -Override slip-system path (`(001)[100]`, identity quaternion) — a different EbsdLib overload with different semantics for both the index and the angle components: - -| Loading | m | symmetry-operator index | Phis (radians) | Lambdas (radians) | -|---|---|---|---|---| -| `[1,2,3]` | `3/7` = 0.4285714285714286 | 3 | 0.6405223126794245 | 1.0068536854342678 | -| `[1,1,1]` | `1/3` = 0.3333333333333333 | 0 | 0.9553166181245093 | 0.9553166181245093 | -| `[0,0,1]` | 0 | 0 | 0 | 0 | +*Class:* 1 (Analytical), with 4 (Invariant) companions. -The three angle columns on this path are `acos` values, i.e. transcendental rather than surds, so they are not covered by `oracle.py`'s exact-arithmetic table. They were recomputed independently to 60 significant digits — `acos(3/sqrt(14)) = atan(sqrt(5)/3) = 0.640522312679424574143…`, `acos(2/sqrt(14)) = atan(sqrt(10)/2) = 1.006853685434267776537…`, `acos(1/sqrt(3)) = atan(sqrt(2)) = 0.955316618124509278163…` — and rounded to the nearest `double`. The asserted tolerance is `1e-6`, far looser than that rounding. +*Applied:* Closed-form cubic slip-system products define expected factors, angles, indices and poles; invariants check physical bounds, loading-scale invariance and cubic-symmetry invariance. -Class 4 invariants asserted alongside the Class 1 values: +*Encoded:* `src/Plugins/OrientationAnalysis/test/ComputeSchmidsTest.cpp` — `Class 1 analytical oracle, auto slip system` (5 loadings), `orientation-matrix convention and equivariance` (4 quaternions), and `override slip system path` (3 loadings). All pass in CI. -- `0 <= m <= 0.5` — the physical bound for a cubic crystal. **This became assertable only after the EbsdLib fix**: EbsdLib <= 3.1.0 reached 0.500090176 at the maximizing direction. Verified over a 400 x 400 direction sweep in `oracle.py`. -- `m == Phis * Lambdas` on the auto path (the stored values are cosines) and `m == cos(Phis) * cos(Lambdas)` on the override path (the stored values are angles). The pair of assertions is what proves the SC-3 unit flip is real behaviour rather than a documentation misreading. -- `Poles[k] == trunc(100 * crystalLoading[k])`, including negative components, which is what distinguishes truncation from rounding. -- Scale invariance: `[1,2,3]` and `[3,6,9]` agree on all five outputs. -- Cubic-symmetry invariance: all four quaternions give the same `m`, `Phis` and `Lambdas`. - -**Class 3 corroboration (not the oracle):** the three canonical FCC anchors fall out of the table — `m = 0.408` for `<001>` and `<011>` loading and `m = 0.272` for `<111>` loading, with `phi = 54.74 deg` / `lambda = 45 deg` and `phi = 70.53 deg` / `lambda = 35.26 deg` respectively (Dieter, *Mechanical Metallurgy* 3rd ed. section 4-3; Schmid & Boas, *Kristallplastizität*). These are quoted as a sanity check on the Class 1 derivation, not as its source. - -**Encoded test references** (all greppable in `src/Plugins/OrientationAnalysis/test/ComputeSchmidsTest.cpp`): - -- `"OrientationAnalysis::ComputeSchmidsFilter: Class 1 analytical oracle, auto slip system"` -- `"OrientationAnalysis::ComputeSchmidsFilter: orientation-matrix convention and equivariance"` -- `"OrientationAnalysis::ComputeSchmidsFilter: override slip system path"` -- `"OrientationAnalysis::ComputeSchmidsFilter: options, skip path and phase guards"` +*Second-engineer review:* No second engineer is named in the sign-off. ## Bugs found and fixed -This branch fixes all defects in this table. The fixes will be in the DREAM3D-NX release after v7.4.1. +All defects in this table are fixed. DREAM3D-NX v7.4.2 contains the fixes for `ComputeSchmidsFilter-D1`, `ComputeSchmidsFilter-D3`, `ComputeSchmidsFilter-D6` and `ComputeSchmidsFilter-D7`. DREAM3D-NX did not use the legacy angle sentinel described in `ComputeSchmidsFilter-D2`. -| Deviation | Defect | Affected released versions | Resolution in this branch | -|---|---|---|---| -| `ComputeSchmidsFilter-D1` | The cubic calculation used truncated normalizers. The calculated Schmid factor could exceed the physical maximum of 0.5. | DREAM.3D 6.5.171; DREAM3D-NX v7.0.0 through v7.4.1. | EbsdLib 3.1.1 uses full-precision constants. | -| `ComputeSchmidsFilter-D2` | DREAM.3D initialized uncomputed angle values to `-301`. The value was an error code that was used as an initialization value. | DREAM.3D 6.5.171 only. DREAM3D-NX was not affected. | SIMPLNX initializes all output arrays to zero. The algorithm also initializes Feature 0. | -| `ComputeSchmidsFilter-D3` | The orientation library did not initialize all output values. The filter could reuse values from the previous Feature. | DREAM.3D 6.5.171; DREAM3D-NX v7.0.0 through v7.4.1. | EbsdLib 3.1.1 initializes all output values. The algorithm also initializes its output variables for each Feature. | -| `ComputeSchmidsFilter-D6` | The filter accepted invalid phase values, empty arrays, arrays with different tuple counts, and zero direction vectors. These inputs could cause invalid output or invalid memory access. | DREAM.3D 6.5.171; DREAM3D-NX v7.0.0 through v7.4.1. | The algorithm rejects invalid phase values. Preflight rejects empty arrays, arrays with different tuple counts, and zero direction vectors. | +| Deviation | Defect | Affected released versions | Resolution | +|-----------|--------|----------------------------|------------| +| `ComputeSchmidsFilter-D1` | Truncated cubic normalizers inflated factors and allowed values above 0.5. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | EbsdLib uses full-precision square-root constants. | +| `ComputeSchmidsFilter-D2` | Skipped legacy angle rows retained the error-code value `-301`. | DREAM.3D v6.5.171 was affected. DREAM3D-NX was not affected. | Skipped output rows contain zero. | +| `ComputeSchmidsFilter-D3` | Library routines left outputs unchanged or read a previous value, which could contaminate the next feature. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | Library routines initialize outputs; the filter also resets local outputs for each feature. | +| `ComputeSchmidsFilter-D6` | Invalid phases, malformed arrays and zero vectors could cause invalid reads or output. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | Preflight checks array sizes and vectors; execution checks phase bounds. | +| `ComputeSchmidsFilter-D7` | Output actions omitted an explicit fill value, so skipped rows depended on store initialization. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected by this omission. | All five output actions explicitly request zero initialization. | ## Code path coverage -`Source: src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeSchmids.cpp (139 lines).` +20 of 25 paths exercised. Each uncovered row states its gap. -The algorithm is flat: a parameter setup block, a sentinel-row write, then one pass over Features. +Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeSchmids.cpp` (139 lines) and `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeSchmidsFilter.cpp` (307 lines). -21 of 22 paths exercised. +Test names below omit the common `OrientationAnalysis::ComputeSchmidsFilter: ` prefix. Preflight validates inputs and declares outputs; execution initializes the sentinel and processes each remaining feature. | # | Phase | Path | Test case | |---|---|---|---| @@ -122,7 +69,7 @@ The algorithm is flat: a parameter setup block, a sentinel-row write, then one p | 2 | *Preflight* | Reject empty Feature arrays (`-13504`) | "preflight input validation" / "Feature arrays must contain the sentinel tuple" | | 3 | *Preflight* | Reject an empty Crystal Structures array (`-13505`) | "preflight input validation" / "Crystal Structures must contain at least one ensemble" | | 4 | *Preflight* | Reject a zero Loading Direction (`-13506`) | "preflight input validation" / "Loading Direction must be non-zero" | -| 5 | *Preflight* | Create the three unconditional output arrays with a zero fill | all executing V&V cases | +| 5 | *Preflight* | Create the three unconditional output arrays with a zero fill | `Class 1 analytical oracle, auto slip system` | | 6 | *Preflight* | Create `Phis`/`Lambdas` when `StoreAngleComponents` is true | "Class 1 analytical oracle, auto slip system" | | 7 | *Preflight* | Skip `Phis`/`Lambdas` when `StoreAngleComponents` is false | "options, skip path and phase guards" / "StoreAngleComponents == false does not create Phis/Lambdas" | | 8 | *Preflight* | Reject a zero override Slip Plane (`-13507`) | "preflight input validation" / "Override Slip Plane must be non-zero" | @@ -134,63 +81,45 @@ The algorithm is flat: a parameter setup block, a sentinel-row write, then one p | 14 | *Execute - per-Feature* | Negative phase id guard (`-13502`) | "options, skip path and phase guards" / "a negative phase id is an error" | | 15 | *Execute - per-Feature* | Out-of-range phase id guard (`-13501`) | "options, skip path and phase guards" / "a phase id beyond the ensemble count is an error" | | 16 | *Execute - per-Feature* | Skip a Feature whose Laue class is at or beyond `LaueGroupEnd` | "options, skip path and phase guards" / "Features whose Laue class is beyond LaueGroupEnd get defined zeros" | -| 17 | *Execute - per-Feature* | Valid Laue class with no enumerated slip systems | "options, skip path and phase guards" / "Valid Laue classes without slip systems keep zero Schmid outputs and computed Poles" | -| 18 | *Execute - per-Feature* | Auto slip-system path (`getSchmidFactorAndSS(load, ...)`) | "Class 1 analytical oracle, auto slip system"; "orientation-matrix convention and equivariance" | -| 19 | *Execute - per-Feature* | Override slip-system path (`getSchmidFactorAndSS(load, plane, direction, ...)`) | "override slip system path" | -| 20 | *Execute - per-Feature* | Store or omit angle components | auto, override, and StoreAngleComponents=false sections | -| 21 | *Execute - per-Feature* | `m_ShouldCancel` early return | *Not directly tested. No cancel-signal injection infrastructure exists for algorithm classes; same gap as the rest of this plugin.* | -| 22 | OOC storage | Quaternion chunk and one-feature tail have different analytical factors and poles | `real HDF5 quaternion-chunk tail oracle` | +| 17 | *Execute - per-Feature* | Triclinic automatic mode has no enumerated slip systems | "options, skip path and phase guards" / "Valid Laue classes without slip systems keep zero Schmid outputs and computed Poles" | +| 18 | *Execute - per-Feature* | Cubic-High automatic slip-system path | "Class 1 analytical oracle, auto slip system"; "orientation-matrix convention and equivariance" | +| 19 | *Execute - per-Feature* | Cubic-High override path, including a zero maximum | "override slip system path" | +| 20 | *Execute - per-Feature* | Store or omit angle components | `Class 1 analytical oracle, auto slip system`; `override slip system path`; `options, skip path and phase guards` | +| 21 | *Execute - per-Feature* | `m_ShouldCancel` early return | *Not directly tested. No fixture injects cancellation.* | +| 22 | Storage | Quaternion chunk and one-feature tail | *Not directly tested. The hidden `real HDF5 quaternion-chunk tail oracle` requires an HDF5 store.* | +| 23 | Execute | Only Feature 0 exists; the feature loop is empty | *Not directly tested. No executing fixture has exactly one feature tuple.* | +| 24 | Library selection | Other Laue classes in automatic mode; all non-Cubic-High classes in override mode | *Not directly tested. Fixtures select Cubic-High in both modes and Triclinic in automatic mode only.* | +| 25 | Options | Override enabled with angle storage disabled | *Not directly tested. All executing override fixtures enable angle storage.* | ## Test inventory -| Test case | Status | Notes | -|---|---|---| -| `"OrientationAnalysis::ComputeSchmidsFilter"` | retired | Removed during adversarial review because `6_6_stats_test_v2.tar.gz` stores prior ComputeSchmids output and is a circular oracle. Its useful algorithmic behavior is covered by the inline Class 1 and Class 4 fixtures. | -| `"OrientationAnalysis::ComputeSchmidsFilter: Class 1 analytical oracle, auto slip system"` | new-for-V&V | 5 `DYNAMIC_SECTION` loadings x (5 outputs + Class 4 invariants + the Feature-0 sentinel row). 236 assertions. | -| `"OrientationAnalysis::ComputeSchmidsFilter: orientation-matrix convention and equivariance"` | new-for-V&V | 4 `DYNAMIC_SECTION` quaternions pinning the `qu2om` convention, the no-transpose property, negative `Poles` truncation and cubic-symmetry invariance. 109 assertions. | -| `"OrientationAnalysis::ComputeSchmidsFilter: override slip system path"` | new-for-V&V | 3 `DYNAMIC_SECTION` loadings on the second EbsdLib overload, including the degenerate zero-Schmid case, plus the radians-versus-cosines assertion and the `-13500` preflight rejection. 93 assertions. | -| `"OrientationAnalysis::ComputeSchmidsFilter: options, skip path and phase guards"` | new-for-V&V | `StoreAngleComponents == false`; a 20 000-Feature `LaueGroupEnd` skip fixture; a valid no-slip-system Laue class; and the `-13501`/`-13502` phase-id guards. 108 assertions. | -| `"OrientationAnalysis::ComputeSchmidsFilter: preflight input validation"` | new-for-V&V | Mismatched/empty Feature arrays, empty Crystal Structures, and zero loading/override vectors (`-13503` through `-13508`). 19 assertions. | -| `"OrientationAnalysis::ComputeSchmidsFilter: SIMPL Backwards Compatibility"` | kept, untouched | 2 `DYNAMIC_SECTION` conversion fixtures (SIMPL 6.5 UUID, SIMPL 6.4 Filter_Name). 33 assertions. | -| `real HDF5 quaternion-chunk tail oracle` | new-for-V&V | Independent HDF5 boundary outputs; 37 assertions across the configured options. | - -**598 assertions total** (236 + 109 + 93 + 108 + 19 + 33), taken from a `ctest --verbose` run rather than tallied by hand. All 6 pass on the reviewed branch. - -Full `OrientationAnalysis::` regression after the internal-review fixes: **268 of 268 unit tests passed** in the EbsdLib 3.1.1 build. +The six ordinary test cases run in CI. All pass in CI. The hidden case is listed for source completeness. -## Test sensitivity verification - -Test sensitivity verification introduces one temporary defect at a time and confirms that the applicable test fails. - -Seven temporary defects were evaluated. Each defect caused the expected V&V test to fail. - -| Temporary defect | Test that detected the defect | Result | -|---|---|---| -| Transpose the orientation matrix before rotating the loading direction. | Orientation-matrix convention and equivariance | Detected | -| Swap quaternion components X and Y when the filter reads the quaternion. | Orientation-matrix convention and equivariance | Detected | -| Round the `Poles` values instead of truncating them. | Class 1 analytical oracle, orientation-matrix convention, override path, and options and guards | Detected | -| Change the EbsdLib cubic slip-system tie-break from `>` to `>=`. | Class 1 analytical oracle | Detected | -| Remove both phase-index guards. | Options, skip path, and phase guards | Detected | -| Disable the Feature-array tuple-count validation. | Preflight input validation | Detected | -| Disable all three zero-vector guards. | Preflight input validation | Detected | - -OOC recertification (2026-09-21): 6/6 target CTest entries pass in both DREAM3DNX builds. The hidden `real HDF5 quaternion-chunk tail oracle` passes 37 assertions in the OOC binary. The input quaternion store reports a 65,536-tuple chunk. At its final full-chunk feature the identity orientation gives Schmid factor 0.5 and poles [70,0,70]. The tail quaternion (0,0,0.6,0.8) rotates the loading to (0.28,0.96,1)/sqrt(2); cubic equivalents give factor 0.48 and poles [19,67,70]. These expectations account for symmetry, unlike a 90-degree control which is symmetry-equivalent. Existing upstream/develop oracles and tolerances are preserved. No production algorithm or historical sign-off is changed, and no fresh legacy binary run is claimed. +| Test case | Status | Notes | +|-----------|--------|-------| +| `OrientationAnalysis::ComputeSchmidsFilter: Class 1 analytical oracle, auto slip system` | new-for-V&V | Five loading variants: `[0,0,1]`, `[1,1,1]`, `[0,1,1]`, `[1,2,3]`, `[3,6,9]`. Checks all five outputs, the sentinel, bounds and the cosine product. | +| `OrientationAnalysis::ComputeSchmidsFilter: orientation-matrix convention and equivariance` | new-for-V&V | Four quaternion variants: identity, 180 degrees about Z, 120 degrees about [111], and 180 degrees about X. Checks factors, indices and signed poles. | +| `OrientationAnalysis::ComputeSchmidsFilter: override slip system path` | new-for-V&V | Three loading variants: `[1,2,3]`, `[1,1,1]`, `[0,0,1]`. Checks all outputs, the positive-factor angle identity and the zero-result sentinel; rejects a nonperpendicular plane/direction pair. | +| `OrientationAnalysis::ComputeSchmidsFilter: options, skip path and phase guards` | new-for-V&V | Checks angle storage off, all skipped rows in a 20,000-feature fixture, Triclinic zero outputs, and errors for negative/out-of-range phases. | +| `OrientationAnalysis::ComputeSchmidsFilter: preflight input validation` | new-for-V&V | Checks unequal feature tuple counts, empty feature/ensemble arrays and three zero-vector errors (`-13503` through `-13508`). | +| `OrientationAnalysis::ComputeSchmidsFilter: real HDF5 quaternion-chunk tail oracle` | new-for-V&V | Hidden. Requires HDF5 stores and a 65,536-tuple quaternion chunk; asserts factors 0.5 and 0.48, pole components and a skipped-row zero. | +| `OrientationAnalysis::ComputeSchmidsFilter: SIMPL Backwards Compatibility` | kept | `SIMPL 6.5 (UUID)` and `SIMPL 6.4 (Filter_Name)` check filter identity, comments, booleans and array paths/names. | ## Exemplar archive -The existing test archive contained output from an earlier run of this filter. It was a circular oracle and was retired for this filter. The replacement tests use inline Class 1 analytical data and Class 4 invariants. - -## Deviations from DREAM.3D 6.5.171 +- **Archive:** `6_6_stats_test_v2.tar.gz` — retired for this filter; no current test consumes it. +- **SHA512:** `e84999dec914d81efce4fc4237c49c9bf32e48381b1e79f58aa4df934f0d7606cd7a948f9a5e7b17a126a7944cc531b531cfdc70756ca3e2207b20734e089723` (retired archive; still registered for other plugin tests). +- **Provenance:** Not applicable to the current inline fixtures in `src/Plugins/OrientationAnalysis/test/ComputeSchmidsTest.cpp`. -See `vv/deviations/ComputeSchmidsFilter.md` for the root cause, affected users, and recommendation for each deviation. +## Deviations from DREAM.3D v6.5.171 -**Fixed in DREAM3D-NX 7.4.2:** `ComputeSchmidsFilter-D3`. +See `vv/deviations/ComputeSchmidsFilter.md` for the root cause, affected users and recommendation. This table lists only Part 1 entries. | Deviation | Observed difference | -|---|---| -| `ComputeSchmidsFilter-D1` | DREAM.3D 6.5.171 and DREAM3D-NX through v7.4.1 used truncated cubic normalizers. The calculated Schmid factor and angle components were too large. | -| `ComputeSchmidsFilter-D2` | DREAM.3D 6.5.171 initialized uncomputed angle values to `-301`. DREAM3D-NX initializes these values to zero. | -| `ComputeSchmidsFilter-D3` | The orientation libraries did not initialize all Schmid output values. The filter could return undefined or previous Feature values. | -| `ComputeSchmidsFilter-D4` | The angle outputs use cosines on the automatic path and radians on the override path. The default output names and error messages also differ. | -| `ComputeSchmidsFilter-D5` | The override path returns an index from the library symmetry table. Different table orders can return different indices for the same physical result. | -| `ComputeSchmidsFilter-D6` | SIMPLNX rejects invalid phase values, malformed arrays, and zero direction vectors. DREAM.3D 6.5.171 accepted these inputs and could produce invalid output or invalid memory access. | +|-----------|---------------------| +| `ComputeSchmidsFilter-D1` | The corrected cubic automatic calculation removes the legacy normalizer bias. | +| `ComputeSchmidsFilter-D2` | Skipped angle rows contain zero instead of `-301`. | +| `ComputeSchmidsFilter-D3` | Initialized library outputs prevent values from a previous feature from being reused. | +| `ComputeSchmidsFilter-D4` | Default angle-array names and preflight error codes differ; mode-dependent angle units are shared behavior. | +| `ComputeSchmidsFilter-D5` | Override indices differ because the libraries order tied symmetry operators differently. | +| `ComputeSchmidsFilter-D6` | SIMPLNX rejects malformed inputs that DREAM.3D v6.5.171 accepted. | diff --git a/src/Plugins/OrientationAnalysis/vv/ConvertOrientationsFilter.md b/src/Plugins/OrientationAnalysis/vv/ConvertOrientationsFilter.md index 5c4793fe6d..0a3f32c8b6 100644 --- a/src/Plugins/OrientationAnalysis/vv/ConvertOrientationsFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/ConvertOrientationsFilter.md @@ -1,118 +1,119 @@ # V&V Report: ConvertOrientationsFilter -| | | -|-----------|--------------------------| -| Plugin | OrientationAnalysis | +| | | +|--------|--------------| +| Plugin | OrientationAnalysis | | SIMPLNX UUID | `501e54e6-a66f-4eeb-ae37-00e649c00d4b` | | SIMPLNX Human Name | Convert Orientation Representation | -| DREAM3D 6.5.171 equivalent | `ConvertOrientations` (SIMPL UUID `e5629880-98c4-5656-82b8-c9fe2b9744de`) — `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/ConvertOrientations.{h,cpp}`; mapped in `OrientationAnalysisLegacyUUIDMapping.hpp` | +| DREAM.3D v6.5.171 equivalent | `ConvertOrientations` (SIMPL UUID `e5629880-98c4-5656-82b8-c9fe2b9744de`) | | Verified commit | ** | | Status | COMPLETE | -| Sign-off | Michael Jackson — 2026-07-16 | +| Sign-off | Michael Jackson, 2026-06-30; second engineer: Jared Duffey, 2026-07-14 | ## At a glance -| Aspect | Current state | -|------------------------|--------------------------| -| Algorithm Relationship | **Rewrite** (plumbing) under the retained SIMPL UUID. Legacy built a vector of 7 `OrientationConverter` subclasses dispatching each pair to its **direct** pairwise transform (e.g. `eu2om`, `eu2cu`); SIMPLNX uses an 8×8 `switch` dispatching to macro-generated convertors that call EbsdLib `input.toX()` directly. Three scope deltas: SIMPLNX **adds Stereographic** (8th type), **restricts input to float32** (legacy accepted float and double, D2), and **drops legacy's in-place Euler sanitization** (D5). | -| Oracle (confirmed) | **Class 3 (Rowenhorst 2015, DOI 10.1088/0965-0393/23/8/083501)** primary + **Class 1 (Analytical)** for Stereographic closed form + **Class 4 (Invariant)** round-trip. *Transform math is owned/tested by EbsdLib itself (`EbsdLib/Source/Test/Orientation*Test.cpp`); this filter test verifies only the filter's value-add — dispatch routing, component striding, preflight.* Encoded in `test/ConvertOrientationsTest.cpp` — 56-pair 8×8 matrix + stereographic closed form; **1032 assertions pass** (in-core, via `ctest`). OOC: single-algorithm filter made OOC-safe via `requireArraysInMemory`; dedicated OOC run skipped (no in-core/OOC dispatch variants — see V&V phase). | -| Code paths enumerated | 7 of 12 exercised; 2 unreachable arms and 3 unasserted guard/cancellation/progress paths remain outside this regression. | -| Tests today | 5 registered target cases plus one hidden HDF5 boundary test. Paired target selections pass 5/5. | -| Exemplar archive | None — values are inline dispatch landmarks in the test source. Unknown-provenance `k_InitValues` **retired** and replaced by EbsdLib-3.0.0-derived values. These are a consistency check against EbsdLib's reference implementation for the transform math (not EbsdLib-independent); genuinely independent pins are the stereographic closed form and seed-0's Rowenhorst-2015 worked-example orientation. | -| Legacy comparison | **Run** (toy fixture, 6 shared eu→X conversions via 6.5.171 PipelineRunner vs nxrunner on byte-identical Euler input). Headline: 4 of 6 bit-identical; max \|Δ\| = **1.78e-6** (cubochoric), measured for eu→X only. 5 deviations: D1 library-generation precision (measured), D2 float64 scope, D3 Stereographic-added, D4 error-code surface, D5 dropped in-place Euler sanitization. See `comparisons/ConvertOrientationsFilter/results/comparison.md`. | -| Bug flags | **None.** Filter matches the independent oracle on all 56 dispatch pairs + stereographic; all 4 deviations are precision/scope/API, not bugs. | -| V&V phase | Historical status and sign-off retained. Section 4.3 recertification adds real-HDF5 analytical boundary evidence and paired runtime checks. | - -For worked instances see `src/Plugins/OrientationAnalysis/vv/BadDataNeighborOrientationCheckFilter.md` and `src/Plugins/OrientationAnalysis/vv/ComputeAvgCAxesFilter.md`. +| Aspect | Current state | +|--------|---------------| +| Algorithm Relationship | Rewrite of `ConvertOrientations` in DREAM.3D v6.5.171. SIMPLNX adds Stereographic, accepts only float32 arrays, and converts Euler inputs without the legacy normalization. | +| Oracle (confirmed) | Class 1 checks the stereographic formula; Class 2 reference values check dispatch and striding in `test/ConvertOrientationsTest.cpp`. The reference values come from EbsdLib and do not independently check its transform equations. | +| Code paths enumerated | 15 of 27 exercised. Gaps include guards, cancellation, progress messages, empty input, and I/O failures. | +| Tests today | 6 test cases: 5 ordinary cases cover conversions and validation, and 1 hidden case checks a 4,096-tuple boundary. The ordinary cases pass in CI. | +| Exemplar archive | None. Expected values and synthetic inputs are in `test/ConvertOrientationsTest.cpp`. | +| Legacy comparison | Not run for the current algorithm; the saved DREAM.3D v6.5.171 comparison is dated 2026-09-17. The algorithm changed after that run. | +| Bug flags | None. | +| V&V phase | COMPLETE. | ## Summary -`ConvertOrientationsFilter` ("Convert Orientation Representation") converts a Float32 orientation array from any one of 8 representations (Euler, Orientation Matrix, Quaternion, Axis-Angle, Rodrigues, Homochoric, Cubochoric, Stereographic) to any other, applying the conversion per-tuple in parallel. The actual transformation equations are delegated to EbsdLib (Rowenhorst 2015) and are verified by EbsdLib's own test suite; this V&V therefore verifies the **filter's value-add only** — that the `(inputType, outputType)` `switch` dispatches to the correct conversion, that components are read/written with the correct per-tuple stride, and that the preflight contract holds — using EbsdLib-3.0.0-derived values as **dispatch landmarks** plus Class 4 round-trip/`isValid` invariants. Legacy 6.5.171 differences (float-vs-double internal math; 7-vs-8 supported types) are then documented as diff-explanation, not correctness checks. +`ConvertOrientationsFilter` converts float32 orientation arrays between eight representations. Three distinct orientations check all 56 cross-type conversions and component strides against EbsdLib reference values; a separate closed-form calculation checks Quaternion-to-Stereographic output. All pass in CI. ## Algorithm Relationship -*Classification:* **Rewrite** (of the filter plumbing) under the retained SIMPL UUID `e5629880-98c4-5656-82b8-c9fe2b9744de`. - -*Evidence:* The SIMPLNX algorithm `Algorithms/ConvertOrientations.cpp` is structurally distinct from legacy `ConvertOrientations::execute()` / `generateRepresentation()` (DREAM3D 6.5.171): - -- **Legacy** built a `QVector` of **7** `OrientationConverter` subclass instances (Euler / OM / Quaternion / AxisAngle / Rodrigues / Homochoric / Cubochoric — **no Stereographic**), called `setInputData()` on the input-type converter, then `convertRepresentationTo(outputType)`, which dispatches each requested pair to its **direct** pairwise transform (`OrientationConverter.hpp:492` `eu2om`, `:517` `eu2cu`→`eu2ho→ho2cu`) — **not** through a quaternion intermediate. Every `toX()` also ran `sanityCheckInputData()`, which for Euler input rewrote the input array in place (D5). Supported **both `float` and `double`** input arrays. -- **SIMPLNX** uses an outer `if`-chain on `OutputType` (8 cases) wrapping an inner `switch` on `InputType` (8 cases) that dispatches to a macro-generated `TO_REP##Convertor` functor calling `inputInstance.to##TO_REP()` (the EbsdLib 2.0 `Orientation` member methods, some direct, some via OM/quaternion). Supports **8** types (adds **Stereographic**) and **float32 only**. -- Per V&V policy, **a Rewrite under the same UUID is a claim of functional equivalence** for the 7 shared types — the Deviations file (Step 8) must defend it. - -*Port-time deltas / material changes:* - -1. **Dispatch rewrite** — legacy converter-class hierarchy → direct `input.toX()` 8×8 switch. Both dispatch to the same direct pairwise transforms, but the underlying library generation differs (legacy OrientationLib vs EbsdLib 3.x), so intermediate float32 round-off can differ — Deviation D1. -2. **Stereographic added** (SIMPLNX type 7) — no legacy equivalent; out of scope for legacy A/B. -3. **float32 only** (SIMPLNX) vs **float + double** (legacy) — legacy `double` arrays carried out the math in double precision; SIMPLNX always float32. Deviation candidate for any pipeline that fed `double` arrays to legacy. +*Classification:* Rewrite -*Material PRs since baseline:* #1468 ("ConvertOrientationsFilter uses an Algorithm Class"), #1301 ("Add missing algorithm classes"), #1472 ("Update to EbsdLib 2.0.0 API"), #1535 ("Remove redundant preflight checks"). #1472 is the one that swapped the conversion API to EbsdLib 2.0 `input.toX()`. +*Evidence:* `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/OrientationAnalysisLegacyUUIDMapping.hpp` maps the legacy UUID to this filter; eight EbsdLib member-function dispatch groups replace seven legacy converter classes, with differences documented in D2 through D5. ## Oracle -*Class:* **3 (Paper-based — Rowenhorst 2015)** primary, **1 (Analytical)** for Stereographic, **4 (Invariant)** companion. +*Class:* 1 (Analytical), with 2 (Reference implementation) for dispatch and striding. -*Citation:* D. Rowenhorst, A. D. Rollett, G. S. Rohrer, M. Groeber, M. Jackson, P. J. Konijnenberg, M. De Graef, "Consistent representations of and conversions between 3D rotations," *Modelling and Simulation in Materials Science and Engineering* **23**(8) 083501 (2015), DOI 10.1088/0965-0393/23/8/083501 — cited throughout `EbsdLib/Source/EbsdLib/Core/OrientationTransformation.hpp`. +*Applied:* Calculate Stereographic as `(qx, qy, qz)/(1 + qw)`; compare each conversion pair with three orientations expressed in all eight representations in `k_Ref`. -*Applied:* The transform equations are implemented and verified **inside EbsdLib** (`EbsdLib/Source/Test/OrientationTest.cpp` exercises the full 8×8 conversion matrix incl. Stereographic with round-trip + `isValid()`; `OrientationTransformationTest.cpp` pins analytical landmarks — identity, `ax2om` 90°-about-Z; `OrientationConverterTest.cpp` pins a Rowenhorst-style Euler→Quaternion exemplar). This filter test does **not** re-verify that math. Instead it takes **one general orientation expressed in all 8 representations** — values generated directly from the same EbsdLib 3.0.0 the filter links (reference implementation, independent of the filter's parallel-convertor plumbing) — and uses them as **dispatch landmarks**: for every `(inputType, outputType)` pair the filter must transform `R[inputType]` into `R[outputType]` within tolerance. Wired to the wrong conversion, the output would be a detectably different number. Multi-tuple input arrays additionally pin the per-tuple component striding. Stereographic specifically is cross-checked against its closed form (`st = (qₓ,qᵧ,q_z)/(1+q_w)`; inverse `ω = 4·atan(|st|), n̂ = st/|st|`) — Class 1. Class 4 round-trip (`A→B→A` ≈ identity) and `isValid()` predicates cover the full matrix cheaply. +*Encoded:* `src/Plugins/OrientationAnalysis/test/ConvertOrientationsTest.cpp::OrientationAnalysis::ConvertOrientations: Stereographic closed form (Class 1)` checks three quaternion fixtures at tolerance `1e-5`; `Dispatch and striding (8x8 matrix)` checks the same three orientations across 56 pairs at `1e-4`. All pass in CI. -*Encoded:* `test/ConvertOrientationsTest.cpp`: -- `"Dispatch and striding (8x8 matrix)"` — 56 `DYNAMIC_SECTION`s (every `(in,out)` pair incl. Stereographic), 3 distinct general orientations per multi-tuple input array, exact-value comparison vs `k_Ref` (EbsdLib-3.0.0-derived landmarks) at tol 1e-4, plus output component-count/tuple-count striding assertions. -- `"Stereographic closed form (Class 1)"` — Quaternion→Stereographic, expected `st = (x,y,z)/(1+w)` computed in-test from the closed form (no EbsdLib call), tol 1e-5. -- **1032 assertions, all pass** (in-core build `NX-Com-Qt69-Vtk96-Rel`). OOC pass pending. - -**Oracle-independence caveat (honest scope):** the `k_Ref` landmarks are generated from EbsdLib 3.0.0 — the same library the filter links — so with respect to the *transform math* the 8×8 dispatch test is a **consistency check against EbsdLib's reference implementation**, not an EbsdLib-independent one. What it independently verifies is the filter's own value-add: dispatch routing and per-tuple striding (a mis-wired switch or stride bug produces a detectably wrong number regardless of the landmark's provenance). Two elements are genuinely EbsdLib-independent: (1) the Stereographic path, checked against its closed form computed in-test with no EbsdLib call (Class 1); (2) seed-0, whose orientation is the Rowenhorst 2015 worked example — its quaternion matches EbsdLib `OrientationConverterTest`'s exemplar `{-0.2919894…, 0.319372, 0.1502762…, 0.8889099…}`, but the ultimate authority for that value is the paper's Table (Class 3), not the EbsdLib fixture. The transform math itself is verified inside EbsdLib's own `OrientationTest.cpp` / `OrientationTransformationTest.cpp` suite, which this V&V relies on rather than duplicates. - -*Second-engineer review:* **Signed off by Michael Jackson (technical authority), 2026-07-16.** +*Second-engineer review:* Jared Duffey, 2026-07-14. ## Bugs found and fixed -No new defect was found during this OOC recertification. Existing fixes and deviation dispositions remain documented in this report and its sidecar. +All defects in this table are fixed. DREAM3D-NX v7.4.1 contains the fix for `ConvertOrientationsFilter-D5`. + +| Deviation | Defect | Affected released versions | Resolution | +|-----------|--------|----------------------------|------------| +| `ConvertOrientationsFilter-D5` | Euler normalization can change the represented rotation, including at the float32 `Phi = pi` boundary. DREAM.3D also overwrites the stored input. | DREAM3D-NX v7.0.0 through v7.4.0 applied the normalization to a local copy. DREAM.3D v6.5.171 was affected. | Convert the supplied Euler tuple without the modulo and sign-flip normalization; preserve the stored input. | ## Code path coverage -7 of 12 paths exercised. Two paths are unreachable through the filter; the component-shape guard, cancellation, and progress messages are not directly asserted. - -Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ConvertOrientations.cpp` (448 lines) + `Filters/ConvertOrientationsFilter.cpp` preflight. Logical phases: (a) filter `preflightImpl` validation, (b) execute dispatch (8×8 output/input `switch`), (c) per-tuple parallel convertor. - -| # | Phase | Path | Test case | -|----|----------------|---------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------| -| 1 | (a) Preflight | `inputType == outputType` → `-67005` | `Equal Representations` (GENERATE over all 8 types) | -| 2 | (a) Preflight | input component shape has >1 dimension → `-67003` | *Not directly tested. Low-value guard; selection params produce 1-D component shapes in normal use.* | -| 3 | (a) Preflight | input component count ≠ expected for input type → `-67004` | `Invalid preflight` — 3-component array declared as Quaternion (expects 4) → `-67004` | -| 4 | (a) Preflight | out-of-range input/output type index → framework `k_Validate_OutOfRange_Error` | `Invalid preflight` (input/output type = 8) | -| 5 | (a) Preflight | valid → `CreateArrayAction` with output type's component count | `Dispatch and striding` (preflight of all 56 pairs) + `Invalid preflight` (does-not-exist) | -| 6 | (b) Dispatch | 56 cross-type arms (8 outputs × 7 inputs, incl. Stereographic) | `Dispatch and striding (8x8 matrix)` — all 56 `DYNAMIC_SECTION`s; `Stereographic closed form` for qu→st | -| 7 | (b) Dispatch | 8 same-type arms (`case == output`) | *Unreachable through the filter — blocked at preflight by path 1.* | -| 8 | (b) Dispatch | 8 `case Type::Unknown: break;` arms | *Unreachable — `ChoicesParameter` range-validates the index (path 4).* | -| 9 | (c) Convertor | per-tuple read `inNumComps` → `input.toX()` → write `outNumComps` (striding) | `Dispatch and striding` — 3 distinct multi-tuple orientations + output component/tuple-count assertions | -| 10 | (c) Convertor | `m_Filter->shouldCancel()` → early return | *Not directly tested. Requires injecting a cancel signal mid-execution; low-value coverage gap.* | -| 11 | Progress | Per-chunk progress message | *Not directly tested. Progress messages are outside this numerical verification scope.* | -| 12 | Bulk conversion | A 4,096-tuple block and one-tuple tail use different component directions | `real HDF5 4096-block stereographic oracle` — [0,0,1/3] followed by [1/3,0,0] | +15 of 27 paths exercised. Each uncovered path has a row below. + +Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ConvertOrientations.cpp` (436 lines) and `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ConvertOrientationsFilter.cpp` (178 lines). + +Test names below omit the `OrientationAnalysis::ConvertOrientations: ` prefix. Each output representation has seven tested input representations. + +| # | Phase | Path | Test case | +|---|-------|------|-----------| +| 1 | Preflight | Equal input and output types return `-67005` | `Equal Representations` — all eight types | +| 2 | Preflight | Multidimensional component shape returns `-67003` | *Not directly tested. No fixture supplies a multidimensional component shape.* | +| 3 | Preflight | Wrong component count returns `-67004` | `Invalid preflight` — three components declared as Quaternion | +| 4 | Preflight | Out-of-range input or output index fails parameter validation | `Invalid preflight` — index 8 for each parameter | +| 5 | Preflight | Valid input creates an output with the required tuple and component counts | `Dispatch and striding (8x8 matrix)` | +| 6 | Dispatch | Convert seven input types to Euler | `Dispatch and striding (8x8 matrix)` — `-> Euler` sections | +| 7 | Dispatch | Convert seven input types to Orientation Matrix | `Dispatch and striding (8x8 matrix)` — `-> OrientationMatrix` sections | +| 8 | Dispatch | Convert seven input types to Quaternion | `Dispatch and striding (8x8 matrix)` — `-> Quaternion` sections | +| 9 | Dispatch | Convert seven input types to Axis-Angle | `Dispatch and striding (8x8 matrix)` — `-> AxisAngle` sections | +| 10 | Dispatch | Convert seven input types to Rodrigues | `Dispatch and striding (8x8 matrix)` — `-> Rodrigues` sections | +| 11 | Dispatch | Convert seven input types to Homochoric | `Dispatch and striding (8x8 matrix)` — `-> Homochoric` sections | +| 12 | Dispatch | Convert seven input types to Cubochoric | `Dispatch and striding (8x8 matrix)` — `-> Cubochoric` sections | +| 13 | Dispatch | Convert seven input types to Stereographic | `Dispatch and striding (8x8 matrix)`; `Stereographic closed form (Class 1)` | +| 14 | Dispatch | Same-type conversion arms | *Not directly tested. Preflight rejects equal types.* | +| 15 | Dispatch | Unknown input type exits the switch | *Not directly tested. Parameter validation rejects this index.* | +| 16 | Conversion | Read, convert, and write distinct tuples with different component strides | `Dispatch and striding (8x8 matrix)` — three distinct tuples per pair | +| 17 | Cancellation | Stop before a chunk when cancellation is requested | *Not directly tested. No fixture injects cancellation.* | +| 18 | Progress | Send the initial message and chunk progress | *Not directly tested. No test asserts messages.* | +| 19 | Conversion | Convert distinct directions at tuples 4,095 and 4,096 | `real HDF5 4096-block stereographic oracle` — expects `[0,0,1/3]` and `[1/3,0,0]` | +| 20 | Preflight | Missing input array fails parameter validation | `Invalid preflight` | +| 21 | Preflight | Non-float32 input fails parameter validation | *Not directly tested. All input arrays in this file use float32.* | +| 22 | I/O | Store an input read error and return it | *Not directly tested. No fixture injects a read failure.* | +| 23 | I/O | Store an output write error and return it | *Not directly tested. No fixture injects a write failure.* | +| 24 | Conversion | Stop a worker after another worker reports an error | *Not directly tested. No fixture injects a concurrent worker failure.* | +| 25 | Conversion | Empty input skips the tuple loop | *Not directly tested. Every execution fixture has tuples.* | +| 26 | Dispatch | Unknown output type reaches the final return | *Not directly tested. Parameter validation rejects this index.* | +| 27 | Conversion | Convert negative Euler angles or float32 `Phi = pi` without normalization | *Not directly tested. The current Euler fixtures use interior angles; D5 has separate saved comparison evidence.* | ## Test inventory | Test case | Status | Notes | |-----------|--------|-------| -| `OrientationAnalysis::ConvertOrientations: Dispatch and striding (8x8 matrix)` | new-for-V&V | Replaces the retired `Valid filter execution`. 56 `DYNAMIC_SECTION`s (every cross-type pair incl. Stereographic), 3 distinct general orientations per multi-tuple input, exact-value vs EbsdLib-3.0.0 landmarks (tol 1e-4) + output component/tuple-count striding checks. ~1020 assertions. | -| `OrientationAnalysis::ConvertOrientations: Stereographic closed form (Class 1)` | new-for-V&V | Quaternion→Stereographic; expected `st=(x,y,z)/(1+w)` computed in-test (no EbsdLib call), tol 1e-5. Independent analytical pin for the type with no legacy equivalent. | -| `OrientationAnalysis::ConvertOrientations: Invalid preflight` | kept | Negative: does-not-exist input path + out-of-range input/output type index (`ChoicesParameter`). | -| `OrientationAnalysis::ConvertOrientations: Equal Representations` | kept | Negative: same input/output type → `-67005`. `GENERATE` over all 8 types. | -| `OrientationAnalysis::ConvertOrientationsFilter: SIMPL Backwards Compatibility` | kept | `DYNAMIC_SECTION` over SIMPL 6.4 + 6.5 conversion fixtures; validates UUID + argument-key conversion. | -| *(retired)* `OrientationAnalysis::ConvertOrientations: Valid filter execution` | retired | Removed: compared against `k_InitValues` of **unknown provenance** (7×7 only, single tuple, no Stereographic) — a circular-oracle risk. Superseded by the 8×8 dispatch test with EbsdLib-derived, cross-validated landmarks. | -| `real HDF5 4096-block stereographic oracle` | new-for-V&V | Independent HDF5 boundary outputs; 16 assertions across the configured options. | - -OOC recertification (2026-09-21): 5/5 target CTest entries pass in both DREAM3DNX builds. The hidden `real HDF5 4096-block stereographic oracle` passes 16 assertions in the OOC binary. The quaternion-to-stereographic oracle is (qx,qy,qz)/(1+qw). The two boundary inputs (0,0,0.6,0.8) and (0.6,0,0,0.8) therefore yield [0,0,1/3] and [1/3,0,0]. Both input and output stores are HDF5-OOC. The five target CTest entries passed in each build; the selection also ran two unrelated ConvertOrientationsToVertexGeometry entries successfully. Existing upstream/develop oracles and tolerances are preserved. No production algorithm or historical sign-off is changed, and no fresh legacy binary run is claimed. +| `OrientationAnalysis::ConvertOrientations: Invalid preflight` | kept | Checks a missing input array, input/output index 8, and a Quaternion input with only three components. | +| `OrientationAnalysis::ConvertOrientations: Dispatch and striding (8x8 matrix)` | new-for-V&V | Checks 56 cross-type pairs on three distinct tuples, output shapes, and all output components against `k_Ref` at `1e-4`. Replaces `Valid filter execution`. | +| `OrientationAnalysis::ConvertOrientations: Stereographic closed form (Class 1)` | new-for-V&V | Checks nine output components against `(qx,qy,qz)/(1+qw)` at `1e-5`, without an EbsdLib call for expected values. | +| `OrientationAnalysis::ConvertOrientations: Equal Representations` | kept | Checks error `-67005` for each of the eight equal input/output types. | +| `OrientationAnalysis::ConvertOrientations: real HDF5 4096-block stereographic oracle` | new-for-V&V | Hidden. Constructs 4,097 tuples and checks the two boundary outputs, an identity component, store format, and tuple dimensions. | +| `OrientationAnalysis::ConvertOrientationsFilter: SIMPL Backwards Compatibility` | kept | `SIMPL 6.5 (UUID)` and `SIMPL 6.4 (Filter_Name)` sections check the filter UUID, comments, and all four converted arguments. | ## Exemplar archive -- **None.** This filter's oracle is encoded as **inline dispatch landmarks** (`k_Ref` in `test/ConvertOrientationsTest.cpp`), not a cached `.dream3d`. The landmarks are generated from EbsdLib 3.0.0 (the same library the filter links) and cross-validated independently (seed-0 quaternion == EbsdLib `OrientationConverterTest` exemplar; all stereographic values == closed-form projection). No `download_test_data()` entry and no provenance sidecar are required. +- **Archive:** None. Fixtures are constructed in `src/Plugins/OrientationAnalysis/test/ConvertOrientationsTest.cpp`. +- **SHA512:** Not applicable. `src/Plugins/OrientationAnalysis/test/CMakeLists.txt` registers no archive for this filter. +- **Provenance:** Inline `k_Ref` comments identify EbsdLib 3.0.0 as the source of the dispatch reference values; no archive sidecar applies. -## Deviations from DREAM3D 6.5.171 +## Deviations from DREAM.3D v6.5.171 -Four documented deviations, all consequences of the plumbing **Rewrite** under the retained UUID. SIMPLNX is independently verified-correct against the oracle, so these are diff-explanation (no bug flags). Comparison run on the toy fixture (6 shared eu→X conversions); see `vv/comparisons/ConvertOrientationsFilter/results/comparison.md` and `vv/deviations/ConvertOrientationsFilter.md`. +See `vv/deviations/ConvertOrientationsFilter.md` for the root cause, affected users, and recommendation for each Part 1 entry. -- `ConvertOrientationsFilter-D1` — float32 differences (measured ≤ **1.78e-6** for eu→X, 4 of 6 conversions bit-identical) from library-generation drift between legacy OrientationLib and EbsdLib 3.x; both dispatch each pair to the same direct pairwise transform (not a quaternion intermediate). -- `ConvertOrientationsFilter-D2` — legacy accepted float64 orientation arrays (double-precision math); SIMPLNX is float32-only (precision / scope reduction). -- `ConvertOrientationsFilter-D3` — SIMPLNX adds the Stereographic representation; no legacy equivalent (new capability). -- `ConvertOrientationsFilter-D4` — preflight error-code surface changed (range validation delegated to `ChoicesParameter`); invalid configs still rejected. -- `ConvertOrientationsFilter-D5` — legacy performed an in-place Euler range-normalization (`fmod` + sign flips) that mutated the input array and, being non-rotation-preserving, changed the converted orientation for out-of-range Euler input; SIMPLNX does neither. Not covered by the in-range A/B. +| Deviation | Observed difference | +|-----------|---------------------| +| `ConvertOrientationsFilter-D1` | Retired: the claimed numerical difference used a local proof version, not DREAM.3D v6.5.171. | +| `ConvertOrientationsFilter-D2` | SIMPLNX accepts only float32 orientation arrays; DREAM.3D also accepts float64. | +| `ConvertOrientationsFilter-D3` | SIMPLNX adds Stereographic input and output. | +| `ConvertOrientationsFilter-D4` | Invalid inputs return different error codes. | +| `ConvertOrientationsFilter-D5` | SIMPLNX preserves Euler input and omits the legacy normalization that can change the rotation. | diff --git a/src/Plugins/OrientationAnalysis/vv/GroupMicroTextureRegionsFilter.md b/src/Plugins/OrientationAnalysis/vv/GroupMicroTextureRegionsFilter.md index 80a691cc63..14908d3d60 100644 --- a/src/Plugins/OrientationAnalysis/vv/GroupMicroTextureRegionsFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/GroupMicroTextureRegionsFilter.md @@ -1,72 +1,68 @@ # V&V Report: GroupMicroTextureRegionsFilter -| | | -|-----------|--------------------------| -| Plugin | OrientationAnalysis | +| | | +|--------|--------------| +| Plugin | OrientationAnalysis | | SIMPLNX UUID | `3f695987-81b1-47c3-8cff-b49cfa219be0` | -| DREAM3D 6.5.171 equivalent | `GroupMicroTextureRegions` (private filter; not registered for pipeline use) | +| SIMPLNX Human Name | Group MicroTexture Regions | +| DREAM.3D v6.5.171 equivalent | `GroupMicroTextureRegions` (private filter; not registered for pipeline use) | | Verified commit | ** | | Status | COMPLETE | -| Sign-off | Michael Jackson — 2026-08-27 Second engineer: Joey Kleingers, 2026-09-20 | +| Sign-off | Michael Jackson, 2026-08-27; second engineer: Joey Kleingers, 2026-09-20 | ## At a glance -| Aspect | Current state | -|------------------------|--------------------------| -| Algorithm Relationship | **Port with minor algorithm improvements and updates for the SIMPLNX API.** The port retains the legacy BFS grouping algorithm and adds reproducibility controls, two-sided Laue validation, and explicit output handling. | -| Oracle (confirmed) | **Class 1 (Analytical) with Class 4 (Invariant) support.** Five inline analytical fixtures derive expected groups from pure-Bunge c-axis angles, phase classes, volumes, and neighbor lists. | -| Code paths enumerated | **14 of 20 exercised.** Six defensive or low-value paths remain uncovered and are listed below. | -| Tests today | **8 ctest cases and 225 assertions.** Tests cover both comparison targets, tolerance acceptance and rejection, mixed-Laue rejection, non-contiguous neighbors, parent-ID randomization, output invariants, and SIMPL conversion. | -| Exemplar archive | **None for this filter.** The oracle data is generated inline in the unit test. | -| Legacy comparison | **Run — DREAM3D-NX versus DREAM.3D 6.6.382 (`107b8d51b`).** DREAM.3D 6.5.171 could not instantiate the filter because it was not registered. | -| Bug flags | **None.** D6 is fixed in this branch. | -| V&V phase | **COMPLETE.** Second-engineer review and sign-off remain. | +| Aspect | Current state | +|--------|---------------| +| Algorithm Relationship | Minor changes. The filter retains the DREAM.3D v6.5.171 grouping method and changes defaults, random-seed controls, validation, and outputs. | +| Oracle (confirmed) | Classes 1 and 4: five analytical fixtures and output invariants in `test/GroupMicroTextureRegionsTest.cpp`. All pass in CI. | +| Code paths enumerated | 20 of 35 exercised. Gaps include cancellation, data-access failures, full cell batches, and selected edge conditions. | +| Tests today | 9 test cases cover analytical grouping, randomization, phase bounds, optional neighbors, and SIMPL conversion. All pass in CI. | +| Exemplar archive | None. The tests construct their inputs inline. | +| Legacy comparison | Not run against DREAM.3D v6.5.171: the filter is private. The 2026-08-27 local proof comparison predates changes to the current algorithm. | +| Bug flags | None | +| V&V phase | COMPLETE. Sign-off is recorded above. | ## Summary -`GroupMicroTextureRegionsFilter` groups neighboring hexagonal Features whose c-axes satisfy a selected angular tolerance. Verification uses inline Class 1 analytical fixtures and Class 4 invariants. The V&V found and fixed one reproducibility defect and documents six active deviations from DREAM.3D 6.5.171. +`GroupMicroTextureRegionsFilter` groups neighboring hexagonal Features whose c-axes satisfy an angular tolerance. Five Class 1 fixtures and Class 4 invariants check the output, and a regression test checks phase-index errors. All pass in CI. ## Algorithm Relationship -**Port with minor algorithm improvements and updates for the SIMPLNX API.** +*Classification:* Minor changes -*Evidence:* SIMPLNX preserves the legacy random-seed selection, BFS traversal, contiguous and optional non-contiguous neighbor processing, c-axis comparison, volume-weighted running average, parent assignment, and optional label shuffle. The SIMPL UUID maps to the SIMPLNX UUID. - -Material updates: - -1. **SIMPLNX API and validation.** SIMPLNX uses selection parameters, output actions, `DataPath` values, and a separate algorithm class. It inlines the legacy `GroupFeatures` base-class control flow. -2. **Reproducibility controls.** SIMPLNX exposes parent-ID randomization and seed controls. It uses one generator per execution. See D2 and D6. -3. **Corrected Laue validation.** SIMPLNX validates both Features when the running average is enabled. See D3. -4. **Output and precision updates.** SIMPLNX retires the unused `Active` output and uses float32 tolerance conversion. See D5 and D7. +*Evidence:* `GroupFeatures::execute()` and `GroupMicroTextureRegions::determineGrouping()` in DREAM.3D v6.5.171 supply the same neighbor traversal and c-axis tests; SIMPLNX adds seed controls, two-sided Laue checks, phase bounds, and revised outputs. ## Oracle -*Class:* **1 (Analytical)** with **4 (Invariant)** support. +*Class:* 1 (Analytical), with 4 (Invariant). -*Applied:* Pure-Bunge angles `(0, Phi, 0)` give a sample-frame c-axis of `(0, sin Phi, cos Phi)`. Therefore, the c-axis distance is derived directly from the selected `Phi` values. Fixed phase classes, volumes, and neighbor lists then give the expected group partition without using DREAM.3D output. +*Applied:* Pure-Bunge angles give known c-axis separations; explicit neighbor lists, phases, and unit volumes determine the expected groups. -*Encoded:* `test/GroupMicroTextureRegionsTest.cpp` contains five Class 1 fixtures: `Pure-Phi Bunge`, `Tolerance Boundary`, `Running Average`, `Mixed-Laue Rejection`, and `Non-contiguous neighbor grouping`. Class 4 assertions verify group count, equivalence classes, cell-to-Feature parent consistency, parent Attribute Matrix size, parent-ID positivity, seed roundtrip, randomization invariance, and same-seed determinism. All eight ctest cases pass. +*Encoded:* `src/Plugins/OrientationAnalysis/test/GroupMicroTextureRegionsTest.cpp` — `Class 1 Analytical (Pure-Phi Bunge)`, `(Tolerance Boundary)`, `(Running Average)`, `(Mixed-Laue Rejection)`, and `Non-contiguous neighbor grouping`: five fixtures, plus `RandomizeParentIds invariants`. All pass in CI. -*Second-engineer review:* Adam L. Pilchak reviewed the metallurgical intent on 2026-08-11. Software implementation and test review are pending PR review. +*Second-engineer review:* Joey Kleingers, 2026-09-20; Adam Pilchak reviewed metallurgical intent on 2026-08-11. ## Bugs found and fixed -This branch fixes the defect in this table. The fix will be in the DREAM3D-NX release after v7.4.1. +All defects in this table are fixed. DREAM3D-NX v7.4.2 contains the fixes for `GroupMicroTextureRegionsFilter-D3` and `GroupMicroTextureRegionsFilter-D6`. The fix for `GroupMicroTextureRegionsFilter-D8` will be in the DREAM3D-NX release after v7.4.2. -| Deviation | Defect | Affected released versions | Resolution in this branch | -|---|---|---|---| -| `GroupMicroTextureRegionsFilter-D6` | The default used a clock-derived seed while the running-average comparison was enabled by default. Repeated runs of the same pipeline could produce different region partitions. | DREAM3D-NX v7.0.0 through v7.4.1. | `UseSeed` is enabled by default. The filter stores the seed so the run can be repeated. | +| Deviation | Defect | Affected released versions | Resolution | +|-----------|--------|----------------------------|------------| +| `GroupMicroTextureRegionsFilter-D3` | The running-average mode could join a cubic Feature to a hexagonal Feature. | No DREAM3D-NX release. DREAM.3D v6.5.171 was affected in its private filter source. | Both Features must have the hexagonal-high Laue class. | +| `GroupMicroTextureRegionsFilter-D6` | A clock-derived default seed made running-average partitions vary between runs. | No DREAM3D-NX release. DREAM.3D v6.5.171 was affected in its private filter source when running average was enabled. | Fixed-seed execution is enabled by default, and the seed is stored. | +| `GroupMicroTextureRegionsFilter-D8` | A positive phase beyond Crystal Structures could cause an out-of-bounds read. | DREAM3D-NX v7.4.2. DREAM.3D v6.5.171 was affected in its private filter source. | A participating reference or neighbor with an invalid positive phase returns `-87001`. | ## Code path coverage -14 of 20 paths exercised. Six defensive or low-value gaps remain. +20 of 35 paths exercised. Each uncovered path and its test gap are listed below. -Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/GroupMicroTextureRegions.cpp` (333 lines). +Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/GroupMicroTextureRegions.cpp` (423 lines); `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/GroupMicroTextureRegionsFilter.cpp` (244 lines). -The algorithm has four logical phases: seed initialization, BFS traversal, grouping decisions, and output finalization. +Short test names below refer to the full names in Test inventory. | # | Phase | Path | Test case | -|---|---|---|---| +|---|-------|------|-----------| | 1 | Seed initialization | Use the supplied RNG seed. | All analytical tests; the Pure-Phi test also verifies the stored seed. | | 2 | Seed initialization | Derive the seed from the system clock. | *Not directly tested. This nondeterministic path cannot have a fixed expected parent array.* | | 3 | BFS traversal | Disable non-contiguous neighbors. | `Pure-Phi Bunge`; `Default contiguous-only neighbor mode` | @@ -74,51 +70,65 @@ The algorithm has four logical phases: seed initialization, BFS traversal, group | 5 | BFS traversal | Return `-99345` when an enabled non-contiguous list is missing. | *Not directly tested. Selection-parameter validation prevents a missing list during normal filter execution.* | | 6 | BFS traversal | Traverse a contiguous neighbor edge. | `Pure-Phi Bunge`; `Tolerance Boundary`; `Running Average`; `Mixed-Laue Rejection` | | 7 | BFS traversal | Traverse a non-contiguous neighbor edge. | `Non-contiguous neighbor grouping` | -| 8 | BFS traversal | Skip an invalid or self neighbor. | *Not directly tested. Valid generated neighbor lists do not contain invalid or self entries.* | +| 8 | BFS traversal | Skip a negative or self neighbor. | *Not directly tested. The fixtures contain no negative or self entries.* | | 9 | Grouping decision | Skip a neighbor that already has a parent. | `Pure-Phi Bunge` exercises reverse edges after a Feature joins a group. | | 10 | Grouping decision | Skip a phase-0 reference or candidate. | *Not directly tested. Feature 0 is not present in the generated neighbor lists.* | | 11 | Grouping decision | Reject different or non-hexagonal Laue classes. | `Mixed-Laue Rejection` verifies 20 cubic/hexagonal pairs. | | 12 | Grouping decision | Compare against the touching Feature. | `Pure-Phi Bunge`; `Tolerance Boundary` | -| 13 | Grouping decision | Compare against and update the volume-weighted running average. | `Running Average`; `Mixed-Laue Rejection` | +| 13 | Grouping decision | Compare against and update the running average with unit volumes. | `Running Average`; `Mixed-Laue Rejection` | | 14 | Grouping decision | Accept direct angular distance within tolerance. | `Pure-Phi Bunge`; `Running Average`; `Non-contiguous neighbor grouping` | -| 15 | Grouping decision | Accept the antipodal `pi - w` branch. | *Not directly tested. The analytical fixtures use c-axes in one hemisphere.* | +| 15 | Grouping decision | Accept the antipodal `pi - w` condition. | *Not directly tested. The analytical fixtures use c-axes in one hemisphere.* | | 16 | Grouping decision | Reject angular distance outside tolerance. | `Pure-Phi Bunge`; `Tolerance Boundary`; `Running Average` | | 17 | Finalization | Keep sequential parent IDs. | All analytical fixtures | | 18 | Finalization | Randomize parent IDs. | `RandomizeParentIds invariants` | -| 19 | Finalization | Return `-87000` when fewer than two parent tuples exist. | *Not directly tested. Valid fixtures contain real Features and produce at least one parent plus the reserved tuple.* | +| 19 | Finalization | Return `-87000` when fewer than two parent tuples exist. | *Not directly tested. Every fixture has Feature tuples and produces parents.* | | 20 | Finalization | Resize the parent Attribute Matrix and backfill cell parent IDs. | `Pure-Phi Bunge`; `RandomizeParentIds invariants` | +| 21 | Grouping decision | Reject a positive reference phase beyond Crystal Structures with `-87001`. | `Phase Index Bounds` | +| 22 | Grouping decision | Reject a positive neighbor phase beyond Crystal Structures with `-87001`. | `Phase Index Bounds` | +| 23 | Grouping decision | Leave an isolated Feature's invalid phase unused. | `Phase Index Bounds` — isolated Feature section | +| 24 | Data access | Read Feature arrays into buffers and write Feature parent IDs successfully. | `Class 1 Analytical (Pure-Phi Bunge)` | +| 25 | Data access | Propagate a failed read of phases, crystal structures, quaternions, or volumes. | *Not directly tested. Requires a failing data store.* | +| 26 | Data access | Propagate a failed Feature-parent write. | *Not directly tested. Requires a failing data store.* | +| 27 | Cell mapping | Map a full 65,536-cell batch and advance to another batch. | *Not directly tested. All executing fixtures have fewer cells.* | +| 28 | Cell mapping | Map a partial final batch. | `Class 1 Analytical (Pure-Phi Bunge)`; `RandomizeParentIds invariants` | +| 29 | Cell mapping | Propagate a failed cell-array read or write. | *Not directly tested. Requires a failing data store.* | +| 30 | Cancellation | Stop before selecting the next group seed. | *Not directly tested. Requires cancellation injection.* | +| 31 | Cancellation | Stop after grouping and before output finalization. | *Not directly tested. Requires cancellation injection.* | +| 32 | Cancellation | Stop during cell-parent mapping. | *Not directly tested. Requires cancellation injection.* | +| 33 | Finalization | Propagate a parent Attribute Matrix resize error. | *Not directly tested. Requires a failing resize.* | +| 34 | Seed search | Stop when all Features have a parent. | `Pure-Phi Bunge`; `RandomizeParentIds invariants` | +| 35 | Seed search | Wrap the search index to zero after the final Feature. | *Not directly tested. No fixture isolates the wrap condition.* | ## Test inventory +All pass in CI. + | Test case | Status | Notes | -|---|---|---| -| `OrientationAnalysis::GroupMicroTextureRegionsFilter: Class 1 Analytical (Pure-Phi Bunge)` | new-for-V&V, modified | Verifies three analytical groups, cell-to-Feature parent consistency, parent Attribute Matrix size, retired `Active` output absence (D5), and seed roundtrip. 39 assertions. | -| `OrientationAnalysis::GroupMicroTextureRegionsFilter: RandomizeParentIds invariants` | new-for-V&V | Verifies equivalence-class preservation, group count, cell-to-Feature consistency, positivity, same-seed determinism, and shuffle execution. 49 assertions. | -| `OrientationAnalysis::GroupMicroTextureRegionsFilter: Class 1 Analytical (Tolerance Boundary)` | new-for-V&V | Verifies one 8 degree acceptance and one 12 degree rejection at a 10 degree tolerance. 21 assertions. | -| `OrientationAnalysis::GroupMicroTextureRegionsFilter: Class 1 Analytical (Running Average)` | new-for-V&V | Verifies that a 0/9/18 degree chain produces two groups under the running-average comparison for every seed order. 20 assertions. | -| `OrientationAnalysis::GroupMicroTextureRegionsFilter: Class 1 Analytical (Mixed-Laue Rejection)` | new-for-V&V | Verifies that 20 aligned cubic/hexagonal pairs remain separate. A temporary D3 restoration caused 14 pair assertions to fail. 37 assertions. | -| `OrientationAnalysis::GroupMicroTextureRegionsFilter: Non-contiguous neighbor grouping` | new-for-V&V | Verifies grouping through an optional non-contiguous list when all contiguous lists are empty. 18 assertions. | -| `OrientationAnalysis::GroupMicroTextureRegionsFilter: Default contiguous-only neighbor mode` | new-for-V&V | Verifies that the default contiguous-only configuration executes successfully. 2 assertions. | -| `OrientationAnalysis::GroupMicroTextureRegionsFilter: SIMPL Backwards Compatibility` | kept, modified | Converts the SIMPL 6.4 and 6.5 fixtures after the obsolete `ActiveArrayName` mapping was removed. 39 assertions. | -| `OrientationAnalysis::GroupMicroTextureRegionsFilter: Valid Filter Execution` (`[UNIMPLEMENTED][!mayfail]`) | retired | The test used empty paths and could not execute the algorithm. The analytical fixtures replace it. | +|-----------|--------|-------| +| `OrientationAnalysis::GroupMicroTextureRegionsFilter: Phase Index Bounds` | kept | Checks `-87001` for invalid reference and neighbor phases; permits an invalid phase on an isolated Feature. | +| `OrientationAnalysis::GroupMicroTextureRegionsFilter: Class 1 Analytical (Pure-Phi Bunge)` | new-for-V&V | Checks three groups, cell-to-Feature parent consistency, parent matrix size, absent `Active` output (D5), and stored seed. | +| `OrientationAnalysis::GroupMicroTextureRegionsFilter: RandomizeParentIds invariants` | new-for-V&V | Checks group membership, group count, cell mapping, positive labels, same-seed determinism, and changed labels when randomization is enabled. | +| `OrientationAnalysis::GroupMicroTextureRegionsFilter: Class 1 Analytical (Tolerance Boundary)` | new-for-V&V | Checks 8-degree acceptance and 12-degree rejection at a 10-degree tolerance. | +| `OrientationAnalysis::GroupMicroTextureRegionsFilter: Class 1 Analytical (Running Average)` | new-for-V&V | Checks that the 0/9/18-degree chain gives two groups at a 10-degree tolerance with unit volumes and seed 42. | +| `OrientationAnalysis::GroupMicroTextureRegionsFilter: Class 1 Analytical (Mixed-Laue Rejection)` | new-for-V&V | Checks that all 20 aligned cubic/hexagonal pairs remain separate. | +| `OrientationAnalysis::GroupMicroTextureRegionsFilter: Non-contiguous neighbor grouping` | new-for-V&V | Checks grouping through the optional list with empty contiguous lists. | +| `OrientationAnalysis::GroupMicroTextureRegionsFilter: Default contiguous-only neighbor mode` | new-for-V&V | Checks successful preflight and execution with optional neighbors disabled; does not assert parent values. | +| `OrientationAnalysis::GroupMicroTextureRegionsFilter: SIMPL Backwards Compatibility` | kept | Checks loaded parameters in `SIMPL 6.5 (UUID)` and `SIMPL 6.4 (Filter_Name)` sections; the obsolete `ActiveArrayName` is ignored. | ## Exemplar archive -- **Archive:** None. The test creates all oracle inputs inline. -- **SHA512:** N/A +- **Archive:** None. All oracle inputs are constructed inline. +- **SHA512:** Not applicable. `src/Plugins/OrientationAnalysis/test/CMakeLists.txt` has no archive registration for this filter. - **Provenance:** `src/Plugins/OrientationAnalysis/vv/provenance/GroupMicroTextureRegionsFilter.md` -## Deviations from DREAM.3D 6.5.171 - -DREAM.3D 6.5.171 contains the filter source but does not register the filter. Therefore, a direct pipeline comparison is not possible. The empirical A/B comparison used DREAM.3D 6.6.382, built from commit `107b8d51b`, because that version registers the filter. Both implementations ran only Group MicroTexture Regions on byte-identical input. The independent Class 1 and Class 4 oracle establishes correctness. The DREAM.3D 6.6.382 comparison provides deviation and migration evidence. +## Deviations from DREAM.3D v6.5.171 -See `vv/deviations/GroupMicroTextureRegionsFilter.md` for the root cause, affected users, and recommendation for each deviation. +See `vv/deviations/GroupMicroTextureRegionsFilter.md` for the root cause, affected users, and recommendation. These differences follow from the private DREAM.3D v6.5.171 source. | Deviation | Observed difference | -|---|---| -| `GroupMicroTextureRegionsFilter-D2` | DREAM.3D always randomizes parent labels. DREAM3D-NX uses reproducible sequential labels by default and provides optional seeded randomization. | -| `GroupMicroTextureRegionsFilter-D3` | With the running average enabled, DREAM.3D can group a non-hexagonal Feature with a hexagonal Feature. DREAM3D-NX validates both Laue classes and rejects the pair. | -| `GroupMicroTextureRegionsFilter-D4` | DREAM.3D defaults to the touching-Feature comparison. DREAM3D-NX defaults to the running-average comparison. | -| `GroupMicroTextureRegionsFilter-D5` | DREAM.3D creates an unused `Active` array. DREAM3D-NX retires this output and retains the parent Attribute Matrix. | -| `GroupMicroTextureRegionsFilter-D6` | Released DREAM3D-NX versions used a clock-derived seed by default. The verified branch enables a fixed seed by default. | -| `GroupMicroTextureRegionsFilter-D7` | DREAM.3D and DREAM3D-NX use different pi precision when converting the tolerance to radians. A candidate at the float32 boundary can produce a different result. | +|-----------|---------------------| +| `GroupMicroTextureRegionsFilter-D2` | DREAM.3D randomizes parent labels by default. SIMPLNX exposes label randomization and fixed-seed controls. | +| `GroupMicroTextureRegionsFilter-D3` | SIMPLNX checks both Features' Laue classes in running-average mode. DREAM.3D checks only the candidate's class. | +| `GroupMicroTextureRegionsFilter-D4` | SIMPLNX defaults to running-average comparison and a 20-degree tolerance. DREAM.3D defaults to touching-Feature comparison and 1 degree. | +| `GroupMicroTextureRegionsFilter-D5` | SIMPLNX omits the unused `Active` output and retains the parent Attribute Matrix. | +| `GroupMicroTextureRegionsFilter-D7` | Float32 tolerance conversion in SIMPLNX can change a decision at the acceptance boundary. | diff --git a/src/Plugins/OrientationAnalysis/vv/NeighborOrientationCorrelationFilter.md b/src/Plugins/OrientationAnalysis/vv/NeighborOrientationCorrelationFilter.md index 00e6491449..698596689f 100644 --- a/src/Plugins/OrientationAnalysis/vv/NeighborOrientationCorrelationFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/NeighborOrientationCorrelationFilter.md @@ -1,152 +1,155 @@ # V&V Report: NeighborOrientationCorrelationFilter -| | | -|-----------|--------------------------| -| Plugin | OrientationAnalysis | -| SIMPLNX UUID | 4625c192-7e46-4333-a294-67a2eb64cb37 | -| DREAM3D 6.5.171 equivalent | NeighborOrientationCorrelation (SIMPL UUID 6427cd5e-0ad2-5a24-8847-29f8e0720f4f) | +| | | +|--------|--------------| +| Plugin | OrientationAnalysis | +| SIMPLNX UUID | `4625c192-7e46-4333-a294-67a2eb64cb37` | +| SIMPLNX Human Name | Neighbor Orientation Correlation | +| DREAM.3D v6.5.171 equivalent | `NeighborOrientationCorrelation` (SIMPL UUID `6427cd5e-0ad2-5a24-8847-29f8e0720f4f`) | | Verified commit | ** | | Status | COMPLETE | -| Sign-off | Michael Jackson — 2026-07-16 | +| Sign-off | Michael Jackson, 2026-07-06; second engineer: Nathan Young, 2026-07-08 | ## At a glance -| Aspect | Current state | -|------------------------|--------------------------| -| Algorithm Relationship | **Port with bug fixes** — statement-for-statement port of `NeighborOrientationCorrelation` with four deliberate correctness deltas: stale-w fix, double-precision quaternion math, restored `6 − Level` pass schedule, argmax neighbor selection. | -| Oracle (confirmed) | **Class 2 (Reference implementation) + Class 1 (Analytical) + Class 4 (Invariant)** — NumPy reference (`reference_noc.py`) with 35 hand-derivation cross-checks; encoded as 12 inline fixtures `Oracle F01`–`F12` + invariant tests in `test/NeighborOrientationCorrelationTest.cpp`, all pass. | -| Code paths enumerated | 20 of 22 exercised; the cancel path and within-pass ascending-copy order dependence remain documented gaps. | -| Tests today | 19 public ctest cases run in both builds — 14 oracle/invariant fixtures (including `Oracle F13`), 1 production-scale Small IN100 invariant test, 2 preflight validation tests, 1 phase/Laue bounds test, and 1 SIMPL backward-compatibility test. The OOC build adds one registered 200³ real-HDF5 boundary test. | -| Exemplar archive | **None — fully retired.** All oracle data is inline (programmatic toy fixtures); the Small IN100 test uses archive-free invariant checks. The v1 archive was retired because its comparison was hollow from its 2022-07-24 introduction (`d199bc749`; archive SHA unchanged since first registration `e34baf1f2`, 2022-12-02), and a regenerated exemplar would be a circular oracle — so no replacement archive exists. | -| Legacy comparison | **Run — SIMPLNX vs DREAM3D 6.5.171**, fixtures + production scale. Fixtures: 6.5.171 differs on exactly the 5 of 12 whose outcome depends on a defect (D1–D3); the 7 fully-tied fixtures are identical because SIMPLNX's argmax resolves ties to the same last-in-scan-order neighbor 6.5.171 picked. Production (Small IN100, 4.44M cells, Level 2): 14.29% of cells differ, decomposed per deviation entry. Each root cause proven by applying the surgical fix to a local build of the legacy source — then bit-identical to SIMPLNX on all 12 fixtures **and all 4,444,713 production cells** (which also bounds D4 precision at zero observed). | -| Bug flags | D1 (legacy stale-w), D2 (double level decrement, was also in SIMPLNX — fixed), D3 (last-wins selection, was also in SIMPLNX — fixed). D4 is precision, not a bug. Plus: hollow exemplar comparison in the v1 test (fixed). | -| V&V phase | All phases complete; V&V signed off 2026-07-16 (Michael Jackson, technical authority). Outstanding: fresh before/after doc screenshots (existing images predate the fixes). | +| Aspect | Current state | +|--------|---------------| +| Algorithm Relationship | Minor changes to `NeighborOrientationCorrelation` in DREAM.3D v6.5.171. SIMPLNX corrects the pass count and neighbor selection, uses double-precision orientations, and transfers data by slice. | +| Oracle (confirmed) | Classes 1 and 4 define analytical tuples and invariants in `test/NeighborOrientationCorrelationTest.cpp`. A Class 2 NumPy reference supplies the 12-fixture comparison evidence in provenance. | +| Code paths enumerated | 27 of 41 exercised. Gaps include transfer-order cases, cancellation, and I/O failures. | +| Tests today | 20 ordinary test cases cover analytical outputs, invariants, invalid inputs, geometry cell tuple counts and SIMPL conversion. One additional hidden case generates input data. | +| Exemplar archive | `Small_IN100_dream3d_v3.tar.gz` supplies production-test input only. The two filter-output exemplars are retired; expected outputs come from analytical fixtures and invariants. | +| Legacy comparison | Not run on the current algorithm; the run against DREAM.3D v6.5.171 on 2026-07-07 predates the slice-transfer changes. | +| Bug flags | None | +| V&V phase | Verification records and regression fixtures are complete. A legacy comparison with the current algorithm remains open. | ## Summary -Neighbor Orientation Correlation replaces low-confidence EBSD cells with the attributes of their "best" face neighbor — the one most orientation-similar to the other neighbors — over `6 − Level` passes. It was verified against a NumPy reference implementation with hand-derived (convention-free, co-axial rotation) expected outputs on 12 fixtures covering every logic branch, which exposed and fixed two inherited legacy defects in SIMPLNX (halved pass count; last-wins instead of arg-max selection; the corrected argmax deliberately resolves ties to the same neighbor 6.5.171 picked, confining migration diffs to genuinely defective decisions). SIMPLNX matches the oracle exactly on all fixtures and at production scale. Four differences from DREAM3D 6.5.171 are documented (D1–D4) — cases where SIMPLNX is the more correct version, quantified on Small IN100 at 14.29% of cells, each root cause proven by a surgical patch to a local build of the legacy source. +`NeighborOrientationCorrelationFilter` replaces low-confidence cells with tuples from the face neighbor that has the highest similarity count over `6 - Level` passes. Analytical fixtures and invariants verify selection, phase handling and tuple transfer. All pass in CI. ## Algorithm Relationship -**Minor changes** (port with deliberate correctness deltas) +*Classification:* Minor changes -*Evidence:* SIMPL UUID `6427cd5e-0ad2-5a24-8847-29f8e0720f4f` maps to this filter in `OrientationAnalysisLegacyUUIDMapping.hpp`; the algorithm body is a statement-for-statement translation of legacy `NeighborOrientationCorrelation::execute()` (same 6-face neighbor order `{-XY, -X, -1, +1, +X, +XY}`, same pair-similarity counting, same per-level in-place tuple transfer; SIMPLNX replaces legacy's last-similar-neighbor-wins selection with a last-of-ties argmax — deviation D3). +*Evidence:* `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/OrientationAnalysisLegacyUUIDMapping.hpp` maps the legacy UUID to this filter; source comparison retains face-neighbor pair counting but changes selection, pass count, precision and transfer order. -**Deliberate deltas from legacy:** - -1. **Stale-misorientation fix** — legacy computes `w = getMisoQuat(...)` only inside the same-phase conditional but tests `w` against the tolerance unconditionally, so a mixed-phase (or phase-0) pair inherits the previous pair's `w`. SIMPLNX re-initializes the axis-angle to `std::numeric_limits::max()` before every pair. Changes output on mixed-phase datasets (see deviations). -2. **float → double quaternion math** — legacy uses `QuatF`/`getMisoQuat` (float32); SIMPLNX uses `ebsdlib::QuatD`/`calculateMisorientation` (float64, angle at index `[3]` of the returned Axis-Angle `W`). Only affects pairs whose misorientation sits within float rounding of the tolerance. -3. **Parallel transfer stage** — legacy TBB `task_group` over arrays vs SIMPLNX `ParallelTaskAlgorithm` over arrays; both copy tuples per-array **in place, in ascending voxel order**. This ordering is a genuine behavioral characteristic, not a no-op: within a single pass, if a replaced cell's chosen source is a lower-indexed cell that is *also* replaced in the same pass, the higher-indexed cell inherits the already-overwritten (chained / double-hop) value rather than the source's original value. SIMPLNX reproduces legacy 6.5.171 here exactly (same ascending in-place copy), so on any given input the two agree — but see the coverage caveat below: the single-bad-cell oracle fixtures do not construct a chaining case, so a hypothetical reference implementation using snapshot-copy semantics would pass them. The parity is established by construction (statement-for-statement port) rather than by a discriminating fixture. -4. **Face-neighbor refactor (PR #1523)** and 2D standardization (PR #1590) — replaced the hand-rolled bounds checks with `NeighborUtilities` (`initializeFaceNeighborOffsets` / `computeValidFaceNeighbors`); offset table and iteration order match legacy exactly. - -**Legacy quirks found ported into SIMPLNX** (disposition per this V&V): +## Oracle -- The level loop double-decremented `currentLevel` (loop `currentLevel--` plus a trailing `currentLevel = currentLevel - 1`), introduced in legacy commit `0bbeb1d49` (2014-08-15) when the original 2013 `while(currentLevel > m_Level)` single-decrement loop was converted to a `for` loop for progress reporting — halving the original design's `6 - Level` pass count. **Fixed in SIMPLNX** (deviation D2). Note: the legacy user doc's own example implied `7 - Level` and never matched any shipped code; the 2013 loop is the statement of intent. -- `best = 0` was reset inside the neighbor-selection loop (present since the filter's 2013 birth), so the *last* valid neighbor with any similar pair won, not the arg-max neighbor. **Fixed in SIMPLNX** (deviation D3) with a last-of-ties argmax so fully-tied neighborhoods still match 6.5.171. -- `neighborDiffCount` was accumulated but never read — the once-documented "at least *Cleanup Level* neighbors must ..." threshold has never been enforced by any version; `currentLevel` never appears in the loop body. **Dead code removed** in SIMPLNX; docs corrected instead of changing behavior. -- `bestNeighbor` persists across passes (re-copying previously repaired cells). Identical in both versions; **characterized, kept as-is**, mirrored by the oracle. +*Class:* 1 (Analytical) and 4 (Invariant), with 2 (NumPy reference implementation) for the archived comparison fixtures. -**Material PRs since baseline:** #1340 (threaded messaging), #1472 (EbsdLib 2.0 API), #1523 (face-neighbor utilities), #1590 (2D image standardization) — all verified structure-preserving by diff inspection. +*Applied:* Hand-derived rotations and similarity counts specify source tuples; invariants require unchanged high-confidence cells, unchanged ignored arrays, and no changes at Level 6. -## Oracle +*Encoded:* `src/Plugins/OrientationAnalysis/test/NeighborOrientationCorrelationTest.cpp::OrientationAnalysis::NeighborOrientationCorrelationFilter: Oracle F01 - uniform neighbors 3D` and F02–F13 provide 13 analytical cases; `Class 4 - Level >= 6 is a no-op (I4)` and `Small IN100 Pipeline` check invariants. All pass in CI. -*Class:* **2 (Reference implementation)**, with Class 1 (Analytical) and Class 4 (Invariant) companions. +*Second-engineer review:* Nathan Young, 2026-07-08. -*Applied:* A NumPy reference implementation of the intended algorithm (`reference_noc.py`, NumPy 2.4.2, pinned in the provenance sidecar) computes expected outputs for 12 synthetic fixtures. The intended selection rule is an argmax over neighbor similarity counts with **last-of-ties** resolution (`>=` with a count > 0 guard), deliberately tie-compatible with 6.5.171. All fixture orientations are co-axial rotations about +Z with deltas ≤ 45° (cubic) / ≤ 30° (hex), where every misorientation-fold convention reduces to `|Δθ|` — so each single-pass fixture is *also* hand-derivable (Class 1; `check_derivations.py`, 35 assertions). Quirk toggles in the same script exactly reproduce stock 6.5.171 behavior, pinning each deviation's root cause. Class 4 invariants: high-CI cells never modified (I1), ignored arrays untouched (I2), transfer only copies existing tuples (I3), Level ≥ 6 is a no-op (I4). +## Bugs found and fixed -*Encoded:* `test/NeighborOrientationCorrelationTest.cpp::Oracle F01..F12` (12 TEST_CASEs) + `::Oracle F13` (NeighborList/String cell-array transfer — verified by construction, not by the reference implementation) + `::Class 4 - Level >= 6 is a no-op (I4)` + `::Preflight - Level validation` — all pass in in-core and OOC builds. Derivations embedded as comments beside each fixture. +All defects in this table are fixed. DREAM3D-NX v7.4.2 contains the fixes for D2, D3, D5 and D6. The fix for D7 will be in the DREAM3D-NX release after v7.4.2. DREAM3D-NX releases were not affected by D1. -*Second-engineer review:* **Signed off by Michael Jackson (technical authority), 2026-07-16.** +| Deviation | Defect | Affected released versions | Resolution | +|-----------|--------|----------------------------|------------| +| `NeighborOrientationCorrelationFilter-D1` | A stale angle lets mixed-phase or phase-zero pairs count as similar. | DREAM.3D v6.5.171 was affected. DREAM3D-NX v7.0.0 through v7.4.2 were not affected. | Count a pair only inside the same-positive-phase condition. | +| `NeighborOrientationCorrelationFilter-D2` | A second level decrement stops cleanup passes early. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | Run exactly `6 - Level` passes for valid levels below 6. | +| `NeighborOrientationCorrelationFilter-D3` | Resetting the best count for every neighbor selects the last positive count instead of the maximum. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | Select the largest count; resolve ties to the last neighbor. | +| `NeighborOrientationCorrelationFilter-D5` | The exemplar test skips every missing array and can pass without comparing output. | DREAM3D-NX v7.0.0 through v7.4.1 test sources. DREAM.3D v6.5.171 was not affected. | Check production-scale snapshots and analytical tuples instead of the retired output archive. | +| `NeighborOrientationCorrelationFilter-D6` | Tuple transfer omits NeighborList and String cell arrays. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | Include both array types in transfer and check their values in F13. | +| `NeighborOrientationCorrelationFilter-D7` | Invalid participating phase or Laue indices cause out-of-bounds access. | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was affected. | Return -580096 for an invalid positive phase and -580097 for an invalid Laue index. | ## Code path coverage -**20 of 22 paths exercised.** Two gaps: the cancel path (path 21) needs cancel-signal injection and is -a low-value guard exercised implicitly by the GUI; and the within-pass in-place ascending-copy -order-dependence (path 18a) is a documented legacy-parity behavior established by construction (the -oracle fixtures do not build a chaining case). Neither is a correctness risk for the isolated-defect -inputs the filter targets. - -Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/NeighborOrientationCorrelation.cpp` (216 lines). - -Logical phases: (a) preflight validation, (b) per-pass voxel scan (neighbor-pair similarity -counting + best-neighbor selection), (c) per-pass in-place tuple transfer, repeated `6 − Level` times. - -| # | Phase | Path | Test case | -|----|-------|------|-----------| -| 1 | (a) Preflight | cell arrays with mismatched tuple counts → error `-580093` | `Preflight Error - Cell array tuple count mismatch` | -| 2 | (a) Preflight | `Level < 0` → error `-580094` | `Preflight - Level validation` | -| 3 | (a) Preflight | `Level >= 6` → warning `-580095`, proceed | `Preflight - Level validation` | -| 4 | (a) Preflight | valid inputs → modified-arrays notification, proceed | every oracle fixture | -| 5 | (b) Scan | `CI >= MinConfidence` → cell skipped | all fixtures (invariant I1 checks) | -| 6 | (b) Scan | invalid face neighbor at volume boundary → skipped | `Oracle F11` (corner, 3 valid), `Oracle F07` (2D) | -| 7 | (b) Scan | linear-index → (x,y,z) decomposition on anisotropic dims | `Oracle F12` (4×5×3) | -| 8 | (b) Scan | pair same phase > 0, `w < tol` → similar, both counts credited | `Oracle F01`, `F03`, `F12` | -| 9 | (b) Scan | pair same phase, `w >= tol` → not counted | `Oracle F02` | -| 10 | (b) Scan | pair with mismatched phases → never similar (D1 guard) | `Oracle F05` | -| 11 | (b) Scan | pair involving phase 0 → never similar | `Oracle F06` | -| 12 | (b) Scan | Laue-class-dependent misorientation fold (hex 60° vs cubic 90° periodicity) | `Oracle F08` — a 58° c-axis pair is similar under hex (folds to 2°) but not cubic (folds to 32°), so the same fixture replaces the center under hex and leaves it untouched under cubic | -| 13 | (b) Select | all counts zero → `bestNeighbor` stays −1, cell untouched | `Oracle F02` | -| 14 | (b) Select | tied counts → last neighbor in −Z…+Z scan order (6.5.171-compatible) | `Oracle F01`, `F07`, `F11` | -| 15 | (b) Select | unequal counts → arg-max wins, last of maxes (D3 fix) | `Oracle F03`, `F05`, `F06`, `F12` | -| 16 | (c) Transfer | all non-ignored cell arrays copied from best neighbor, including NeighborList and String types | `Oracle F01` (all 6 numeric arrays verified), `Oracle F13` (NeighborList + String) | -| 17 | (c) Transfer | ignored arrays excluded | `Oracle F09` | -| 18 | (b+c) | multi-pass chaining: inherited CI enables next-pass fills (D2 fix) | `Oracle F04` (Level 2), `Oracle F10` (Level 4) | -| 18a | (c) Transfer | **within-pass** in-place ascending-order copy: a replaced cell sourcing from a lower-indexed cell also replaced in the same pass inherits the chained value | *Not covered by a discriminating fixture — the oracle fixtures replace a single isolated bad cell whose source is a good cell, so no chaining occurs. Parity with legacy 6.5.171's ascending in-place copy is established by construction (statement-for-statement port). Known coverage gap; the behavior is documented in the Algorithm Relationship section.* | -| 19 | (b+c) | `Level >= 6` → zero passes, output identical to input | `Class 4 - Level >= 6 is a no-op (I4)` | -| 20 | (b) | 2D image (`dims[2] == 1`) degenerate-z validity masks | `Oracle F05`, `F07` | -| 21 | (b+c) | cancel requested → abort scan / abort transfer tasks | *Not directly tested. Requires cancel-signal injection; low-value guard.* | - -Non-algorithm coverage (not code paths of the algorithm, tracked in the test inventory): -SIMPL 6.4/6.5 JSON parameter conversion (`SIMPL Backwards Compatibility`, 2 DYNAMIC_SECTIONs), -phase and Laue index bounds (`Phase and Laue Index Bounds`), the production-scale invariant -verification (`Small IN100 Pipeline`, 4.44M cells, Level 2), and the OOC-only 200³ storage-boundary test. +27 of 41 paths exercised. Each uncovered path states its missing fixture below. + +Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/NeighborOrientationCorrelation.cpp` (359 lines), `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/NeighborOrientationCorrelationFilter.cpp` (249 lines), and `src/simplnx/Utilities/SliceBufferedTransfer.hpp` (364 lines). + +Test names below omit the common `OrientationAnalysis::NeighborOrientationCorrelationFilter: ` prefix. Each pass scans and transfers one Z slice at a time. + +| # | Phase | Path | Test case | +|---|-------|------|-----------| +| 1 | Preflight | Cell tuple counts differ → error -580093 | `Preflight Error - Cell array tuple count mismatch (-580093)` | +| 2 | Preflight | Level < 0 → error -580094 | `Preflight - Level validation (-580094 error, -580095 warning)` | +| 3 | Preflight | Level >= 6 → warning -580095 | `Preflight - Level validation (-580094 error, -580095 warning)` | +| 4 | Preflight | Valid inputs → proceed and report modified arrays | `Oracle F01 - uniform neighbors 3D` | +| 5 | Scan | Confidence is at least the threshold → skip | `Oracle F01 - uniform neighbors 3D`; `Small IN100 Pipeline` | +| 6 | Scan | Face neighbor is outside the image → skip | `Oracle F11 - volume corner`; `Oracle F07 - 2D image` | +| 7 | Scan | Unequal X, Y and Z strides → locate the correct neighbors | `Oracle F12 - anisotropic dims` | +| 8 | Scan | Same positive phase and angle below tolerance → count both neighbors | `Oracle F01 - uniform neighbors 3D` | +| 9 | Scan | Angle is at or above tolerance → do not count | `Oracle F02 - dissimilar neighbors untouched` | +| 10 | Scan | Different phases → do not compare orientations | `Oracle F05 - mixed-phase pair never similar (D1 regression)` | +| 11 | Scan | Phase zero → do not count | `Oracle F06 - phase-0 neighbors never counted`; `Phase and Laue Index Bounds` | +| 12 | Scan | Hexagonal symmetry folds a 58° pair to 2° → replace | `Oracle F08 - Laue-class folding (hex vs cubic)` — Hexagonal-High section | +| 13 | Scan | Cubic symmetry folds the same pair to 32° → keep input | `Oracle F08 - Laue-class folding (hex vs cubic)` — Cubic-High section | +| 14 | Select | All similarity counts are zero → no source | `Oracle F02 - dissimilar neighbors untouched` | +| 15 | Select | Counts tie → select the last valid maximum | `Oracle F01 - uniform neighbors 3D`; `Oracle F11 - volume corner` | +| 16 | Select | Counts differ → select the maximum | `Oracle F03 - argmax selection (D3 regression)`; `Oracle F12 - anisotropic dims` | +| 17 | Transfer | Numeric arrays copy through SliceBufferedTransferOneZ | `Oracle F01 - uniform neighbors 3D`; `Oracle F12 - anisotropic dims` | +| 18 | Transfer | NeighborList and String arrays copy their source tuples | `Oracle F13 - NeighborList and String cell arrays transferred` | +| 19 | Transfer | Ignored numeric array → exclude from transfer | `Oracle F09 - ignored arrays untouched (I2)` | +| 20 | Passes | Later passes use the preceding pass output | `Oracle F04 - pass schedule Level 2 (D2 regression)`; `Oracle F10 - pass schedule Level 4 (D2 regression)` | +| 21 | Transfer | Adjacent replaced cells depend on snapshot versus ascending-copy order | *Not directly tested. No fixture asserts distinct source payloads for two replaced cells in the same slice.* | +| 22 | Passes | Level >= 6 → no cleanup passes | `Class 4 - Level >= 6 is a no-op (I4)` — Level 6 | +| 23 | Setup | Single-slice image → omit Z-neighbor reads | `Oracle F07 - 2D image` | +| 24 | Validation | Participating positive phase exceeds ensemble bounds → error -580096 | `Phase and Laue Index Bounds` | +| 25 | Validation | Participating Laue index exceeds operator bounds → error -580097 | `Phase and Laue Index Bounds` | +| 26 | Scan | Negative phase → do not count | *Not directly tested. The nonpositive-phase fixture uses zero only.* | +| 27 | Setup | Read initial slices; rotate the window and stop reads at the final slice | `Oracle F01 - uniform neighbors 3D`; `Oracle F12 - anisotropic dims` | +| 28 | Transfer | Clear source marks after each slice instead of retaining them across passes | *Not directly tested. No fixture isolates a repaired cell whose old source changes in a later pass.* | +| 29 | Cancellation | Cancel at a slice boundary → return | *Not directly tested. Requires cancellation injection.* | +| 30 | I/O | Crystal Structures read fails → return error | *Not directly tested. Requires an injected ensemble read failure.* | +| 31 | I/O | Initial quaternion slice read fails → return error | *Not directly tested. Requires an injected quaternion read failure.* | +| 32 | I/O | Initial phase slice read fails → return error | *Not directly tested. Requires an injected phase read failure.* | +| 33 | I/O | Second initial quaternion slice read fails → return error | *Not directly tested. Requires an injected quaternion read failure.* | +| 34 | I/O | Second initial phase slice read fails → return error | *Not directly tested. Requires an injected phase read failure.* | +| 35 | I/O | Quaternion read after window rotation fails → return error | *Not directly tested. Requires an injected quaternion read failure.* | +| 36 | I/O | Phase read after window rotation fails → return error | *Not directly tested. Requires an injected phase read failure.* | +| 37 | I/O | Confidence slice read fails → return error | *Not directly tested. Requires an injected confidence read failure.* | +| 38 | I/O | Numeric transfer read or write fails → return error | *Not directly tested. Requires an injected transfer failure.* | +| 39 | Transfer | No source marks → preserve numeric tuples without a slice write | `Oracle F02 - dissimilar neighbors untouched` | +| 40 | Transfer | CopyArrayTuple receives a numeric array → copyTuple | *Not directly tested. The caller routes numeric arrays through SliceBufferedTransferOneZ.* | +| 41 | Preflight | Confidence Index, Cell Phases and Cell Quaternions have equal tuple counts that differ from the geometry cell count → error -580098 | `Geometry cell tuple counts` — 4 or 12 tuples for 8 cells | ## Test inventory +All names are in `src/Plugins/OrientationAnalysis/test/NeighborOrientationCorrelationTest.cpp`. There are 20 ordinary cases and one hidden data generator. The hidden data generator is listed separately from correctness checks. + | Test case | Status | Notes | |-----------|--------|-------| -| `Small IN100 Pipeline` | kept (modified) | Runs the 6-filter Small IN100 chain + this filter, then verifies the Class 4 invariants at 4.4M cells against a pre-filter snapshot of all 8 CellData arrays (high-confidence cells untouched everywhere; every modified cell was low-confidence; ≥ 1 cell modified). The in-core build uses resident stores and the OOC build verifies the same assertions with actual HDF5-OOC stores. **Modified for V&V:** the previous exemplar comparison was hollow — bisect-proven hollow from its introduction (`d199bc749`, 2022-07-24: already mapped to `Exemplar Data` with a silent `continue`; archive SHA512 unchanged from `e34baf1f2`, 2022-12-02, to retirement) — and a regenerated exemplar would be a circular oracle, so the archive dependency was removed entirely. | -| `Preflight Error - Cell array tuple count mismatch (-580093)` | kept | Verifies the `validateNumberOfTuples` guard and error code. | -| `Preflight - Level validation (-580094 error, -580095 warning)` | new-for-V&V | 3 SECTIONs: negative Level errors; Level ≥ 6 warns (zero passes); Level < 6 does not warn. | -| `Phase and Laue Index Bounds` | new-for-OOC recertification | Uses OOC-aware stores and verifies that invalid participating phase and Laue indices return their exact execution errors, while invalid indices on skipped cells do not fail execution. | -| `SIMPL Backwards Compatibility` | kept | 2 DYNAMIC_SECTIONs (6.5 UUID / 6.4 Filter_Name), 9 argument checks each. | -| `Oracle F01 - uniform neighbors 3D` | new-for-V&V | Full-tuple snapshot verify of the replacement (+Z, last of 6 tied counts) + I1 on all other cells (6 arrays × 125 cells). | -| `Oracle F02 - dissimilar neighbors untouched` | new-for-V&V | Whole-volume untouched verify; covers the no-similar-pair path. | -| `Oracle F03 - argmax selection (D3 regression)` | new-for-V&V | Count-3 clique vs count-1 pair; the primary pin that the max wins over a later low count. | -| `Oracle F04 - pass schedule Level 2 (D2 regression)` | new-for-V&V | 3×3×3 cascade fully filled in 4 passes; the legacy schedule's 2 passes left the center bad. | -| `Oracle F10 - pass schedule Level 4 (D2 regression)` | new-for-V&V | Same cascade, 2 passes: only the cube center remains unfilled. | -| `Oracle F05 - mixed-phase pair never similar (D1 regression)` | new-for-V&V | Pins the fresh-misorientation behavior; asserts phase never crosses. | -| `Oracle F06 - phase-0 neighbors never counted` | new-for-V&V | Unindexed neighbors excluded from counting and selection. | -| `Oracle F07 - 2D image` | new-for-V&V | Degenerate-z masks; 4-neighbor tie resolves to +Y (last in scan order). | -| `Oracle F08 - Laue-class folding (hex vs cubic)` | new-for-V&V | Discriminating Laue-class test: a 58° c-axis neighbor pair is similar under hex (folds to 2°) but not cubic (folds to 32°). Two sections run the identical fixture under Hexagonal-High and Cubic-High and assert opposite outcomes (center replaced vs untouched), so it catches a folding-periodicity bug in either dispatch. | -| `Oracle F09 - ignored arrays untouched (I2)` | new-for-V&V | IgnoredDataArrayPaths honored; all other arrays copied. | -| `Oracle F13 - NeighborList and String cell arrays transferred` | new-for-V&V | F01 geometry plus a cell-level NeighborList and StringArray; pins that both are copied into the replaced cell (matching 6.5.171) while every other cell keeps its own values. | -| `Oracle F11 - volume corner` | new-for-V&V | 3-valid-neighbor boundary case. | -| `Oracle F12 - anisotropic dims` | new-for-V&V | 4×5×3 (nx≠ny≠nz) so stride/axis-swap bugs cannot hide behind dimension symmetry; secondary D3 pin (last-of-maxes beats a later count-1 pair). | -| `Class 4 - Level >= 6 is a no-op (I4)` | new-for-V&V | Default parameter value performs zero passes (now also surfaced as preflight warning `-580095`). | -| `200x200x200 Large OOC` | new-for-OOC recertification | OOC-only registered test with actual HDF5-OOC input stores. It processes and inspects one Z slice at a time and asserts that the filter modifies at least one low-confidence block-boundary or deterministic-noise cell. | - -All 19 public cases pass in both DREAM3D-NX builds. The OOC-only 200³ boundary case also passes in the OOC build. +| `OrientationAnalysis::NeighborOrientationCorrelationFilter: Small IN100 Pipeline` | kept | Uses the Small IN100 input archive. Replaces the skipped exemplar comparison with snapshots of all numeric cell arrays; checks that only low-confidence cells change and that at least one changes (D5). | +| `OrientationAnalysis::NeighborOrientationCorrelationFilter: Preflight Error - Cell array tuple count mismatch (-580093)` | kept | Checks error -580093 for nine phase tuples and ten confidence/quaternion tuples. | +| `OrientationAnalysis::NeighborOrientationCorrelationFilter: Preflight - Level validation (-580094 error, -580095 warning)` | new-for-V&V | Checks a negative Level error, a Level 6 warning, and no warning at Level 2. | +| `OrientationAnalysis::NeighborOrientationCorrelationFilter: Phase and Laue Index Bounds` | kept | Checks execution errors -580096 and -580097 for participating invalid indices, and success with phase-zero neighbors (D7). | +| `OrientationAnalysis::NeighborOrientationCorrelationFilter: SIMPL Backwards Compatibility` | kept | Dynamic sections `SIMPL 6.5 (UUID)` and `SIMPL 6.4 (Filter_Name)` check filter identity and converted scalar and path arguments. | +| `OrientationAnalysis::NeighborOrientationCorrelationFilter: Oracle F01 - uniform neighbors 3D` | new-for-V&V | Checks all six numeric arrays over 125 cells. Only cell 62 copies cell 87, the final tied neighbor. | +| `OrientationAnalysis::NeighborOrientationCorrelationFilter: Oracle F02 - dissimilar neighbors untouched` | new-for-V&V | Checks that all six numeric arrays stay unchanged when no neighbor pair is similar. | +| `OrientationAnalysis::NeighborOrientationCorrelationFilter: Generate Test Data` | kept | Hidden (`[.GenerateTestData]`). Writes a 200×200×200 synthetic input with 25-cell blocks; does not execute this filter or check its output. | +| `OrientationAnalysis::NeighborOrientationCorrelationFilter: Oracle F03 - argmax selection (D3 regression)` | new-for-V&V | Checks that a count-3 group wins over a later count-1 pair; compares all six numeric arrays. | +| `OrientationAnalysis::NeighborOrientationCorrelationFilter: Oracle F04 - pass schedule Level 2 (D2 regression)` | new-for-V&V | Checks that four passes fill the bad region and copy identity quaternions throughout the volume. | +| `OrientationAnalysis::NeighborOrientationCorrelationFilter: Oracle F10 - pass schedule Level 4 (D2 regression)` | new-for-V&V | Checks that two passes leave only the cube center below the confidence threshold. | +| `OrientationAnalysis::NeighborOrientationCorrelationFilter: Oracle F05 - mixed-phase pair never similar (D1 regression)` | new-for-V&V | Checks that cell 12 copies cell 11, not the different-phase neighbor; compares all six numeric arrays. | +| `OrientationAnalysis::NeighborOrientationCorrelationFilter: Oracle F06 - phase-0 neighbors never counted` | new-for-V&V | Checks that cell 62 copies cell 57 and that phase-zero neighbors do not contribute. | +| `OrientationAnalysis::NeighborOrientationCorrelationFilter: Oracle F07 - 2D image` | new-for-V&V | Checks that cell 12 copies the final valid +Y neighbor, cell 17, in a 5×5×1 image. | +| `OrientationAnalysis::NeighborOrientationCorrelationFilter: Oracle F08 - Laue-class folding (hex vs cubic)` | new-for-V&V | Checks both symmetry modes: a 58° pair folds to 2° under hexagonal symmetry and replaces the center, but to 32° under cubic symmetry and leaves it unchanged. | +| `OrientationAnalysis::NeighborOrientationCorrelationFilter: Oracle F09 - ignored arrays untouched (I2)` | new-for-V&V | Checks that ignored Payload2 stays unchanged while all other numeric arrays copy the selected source. | +| `OrientationAnalysis::NeighborOrientationCorrelationFilter: Oracle F13 - NeighborList and String cell arrays transferred` | new-for-V&V | Checks all numeric, NeighborList and String tuples; only the center copies cell 87 (D6). | +| `OrientationAnalysis::NeighborOrientationCorrelationFilter: Oracle F11 - volume corner` | new-for-V&V | Checks that corner cell 0 copies cell 16 with only three valid face neighbors. | +| `OrientationAnalysis::NeighborOrientationCorrelationFilter: Oracle F12 - anisotropic dims` | new-for-V&V | Checks that cell 29 copies cell 28 on a 4×5×3 image, with unequal similarity counts and axis strides. | +| `OrientationAnalysis::NeighborOrientationCorrelationFilter: Class 4 - Level >= 6 is a no-op (I4)` | new-for-V&V | Executes Level 6 and compares every numeric tuple with its input snapshot. | +| `OrientationAnalysis::NeighborOrientationCorrelationFilter: Geometry cell tuple counts` | new-for-V&V | Checks error -580098 for 4 or 12 tuples per input on an 8-cell geometry, and error -580093 when one input has 4 tuples and the others have 8. Matching counts permit inputs in the same or separate Attribute Matrices; execution repairs only arrays in the Confidence Index matrix and preserves ignored arrays. | ## Exemplar archive -- **Archive:** None — this filter has no exemplar-archive dependency. All oracle data is - built inline by the test fixtures, and the production-scale test verifies invariants - against a pre-filter snapshot. -- **Retired:** `neighbor_orientation_correlation.tar.gz` (v1 — hollow comparison: the - container name mismatch caused every array lookup to fail and the silent `continue` to - skip all comparisons. Bisect-proven hollow from birth: the loop was introduced already - mapping to `Exemplar Data` in `d199bc749` (2022-07-24) and the archive SHA512 never - changed from first registration, `e34baf1f2` 2022-12-02, through retirement) and - `neighbor_orientation_correlation_v2.tar.gz` (a regression pin generated from SIMPLNX's - own output is a circular oracle, which the V&V policy forbids). -- **Provenance (retirement record):** `src/Plugins/OrientationAnalysis/vv/provenance/neighbor_orientation_correlation_v2.md` - -## Deviations from DREAM3D 6.5.171 - -Comparison run (SIMPLNX vs DREAM3D 6.5.171) on all 12 legacy-native V&V fixtures and at production scale (Small IN100, 4,444,713 cells: 14.29% of cells differ; full decomposition in the deviations file); both binaries matched their reference-implementation predictions exactly. Root causes were proven by applying the D1–D3 fixes to a local build of the legacy source, which then reproduced SIMPLNX's output bit-for-bit on all fixtures and on every production cell. - -- `NeighborOrientationCorrelationFilter-D1` — 6.5.171 stale-`w` can replace a cell with **different-phase** data — see `vv/deviations/NeighborOrientationCorrelationFilter.md` -- `NeighborOrientationCorrelationFilter-D2` — 6.5.171 (and SIMPLNX releases before this fix) ran half the intended cleanup passes (measured: 115,380 Small IN100 cells filled only by SIMPLNX) — see `vv/deviations/NeighborOrientationCorrelationFilter.md` -- `NeighborOrientationCorrelationFilter-D3` — 6.5.171 (and SIMPLNX releases before this fix) copied from the last similar neighbor, not the most similar; ties are unaffected by design — see `vv/deviations/NeighborOrientationCorrelationFilter.md` -- `NeighborOrientationCorrelationFilter-D4` — float32 vs float64 misorientation precision (latent; measured zero effect on Small IN100) — see `vv/deviations/NeighborOrientationCorrelationFilter.md` +- **Archive:** `Small_IN100_dream3d_v3.tar.gz` (input only; no filter-output exemplar). +- **SHA512:** `bfa9547e787b0f8e8122702da0eb4e519f48a48bf4f94aad020f72479d071d32dfc96a1425705874c68507a61ed391d28606d9c4f4acd559043ef0ace64fd33f` +- **Provenance:** `src/Plugins/OrientationAnalysis/vv/provenance/NeighborOrientationCorrelationFilter_inputs.md`; output-archive retirement and comparison evidence: `src/Plugins/OrientationAnalysis/vv/provenance/neighbor_orientation_correlation_v2.md`. + +## Deviations from DREAM.3D v6.5.171 + +See `vv/deviations/NeighborOrientationCorrelationFilter.md` for the root cause, affected users and recommendation. This table lists only Part 1 entries. + +| Deviation | Observed difference | +|-----------|---------------------| +| `NeighborOrientationCorrelationFilter-D1` | Mixed-phase and phase-zero pairs cannot inherit an earlier pair's angle. | +| `NeighborOrientationCorrelationFilter-D2` | Cleanup uses `6 - Level` passes instead of half that number, rounded up. | +| `NeighborOrientationCorrelationFilter-D3` | The highest similarity count wins; equal maxima retain the legacy last-neighbor rule. | +| `NeighborOrientationCorrelationFilter-D4` | Misorientation uses double precision instead of single precision. | +| `NeighborOrientationCorrelationFilter-D8` | Numeric tuples use a snapshot per slice, and source marks reset after each slice; DREAM.3D uses ascending in-place copies and retains marks across passes. | diff --git a/src/Plugins/OrientationAnalysis/vv/ReadAngDataFilter.md b/src/Plugins/OrientationAnalysis/vv/ReadAngDataFilter.md index 9809ded962..cb481685c6 100644 --- a/src/Plugins/OrientationAnalysis/vv/ReadAngDataFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/ReadAngDataFilter.md @@ -1,144 +1,127 @@ # V&V Report: ReadAngDataFilter -| | | -|-----------|--------------------------| -| Plugin | OrientationAnalysis | +| | | +|--------|--------------| +| Plugin | OrientationAnalysis | | SIMPLNX UUID | `5b062816-79ac-47ce-93cb-e7966896bcbd` | | SIMPLNX Human Name | Read EDAX EBSD Data (.ang) | -| DREAM3D 6.5.171 equivalent | `ReadAngData` (SIMPL UUID `b8e128a8-c2a3-5e6c-a7ad-e4fb864e5d40`) — `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/ReadAngData.{h,cpp}` | +| DREAM.3D v6.5.171 equivalent | `ReadAngData` (SIMPL UUID `b8e128a8-c2a3-5e6c-a7ad-e4fb864e5d40`) | | Verified commit | ** | | Status | COMPLETE | -| Sign-off | Michael Jackson — 2026-07-07 | +| Sign-off | Michael Jackson, 2026-07-07; second engineer: Jared Duffey, 2026-07-23 | ## At a glance -| Aspect | Current state | -|------------------------|--------------------------| -| Algorithm Relationship | **Minor changes.** Faithful port of legacy `ReadAngData` control flow with 4 deliberate deltas: material-name trim added (D1), TEM/ACOM Nanometer-units detection dropped (D2, obsolete file variants), non-contiguous phase-index handling fixed (D3, legacy crashes), and error-code renumbering (D4). The legacy PIMPL file-cache was dropped (no output effect). | -| Oracle (confirmed) | **Confirmed.** **Class 1 (analytical) + Class 4 (invariant)**, scoped to the filter's value-add per the "don't re-test upstream" rule — EbsdLib (vcpkg 3.0.0) owns `.ang` parsing and is trusted (Class 2 boundary). A hand-authored inline toy `.ang` (3×2 grid, 2 phases, phase-0 points, all values float32-exact) with every expected value hand-derived from the fixture text. Encoded as 7 TEST_CASEs in `test/ReadAngDataTest.cpp`; all pass. SIMPLNX matched the oracle on every fixture with zero discrepancies. | -| Code paths enumerated | 14 of 18 paths exercised (see Code path coverage); the gaps are one unreachable dead branch (`phases.empty()`), two file-changed/malformed-input guards (`-19503`, `-19504`), and the cancel-signal paths — all untestable without injection. | -| Tests today | 8 registered cases plus one hidden HDF5 boundary test. Paired CTest selections pass 8/8. | -| Exemplar archive | **None — retired `read_ang_test.tar.gz`** (circular oracle: the exemplar `.dream3d` was generated from this filter's own output). `download_test_data()` entry removed from `test/CMakeLists.txt`; retirement documented in `vv/provenance/read_ang_test.md`. | -| Legacy comparison | **Run (2026-07-07) vs the official DREAM3D 6.5.171 release.** Three fixtures: hand-authored toy, Small IN100 `Slice_1.ang` (189×201 production scan), non-contiguous-phase toy. On the two supported-format fixtures **all numeric outputs are bit-identical** (cell arrays, ensemble arrays, geometry). Differences: MaterialName trailing space (D1) and the non-contiguous-phase fixture, where **6.5.171 segfaults** (D3). | -| Bug flags | One **legacy** bug, empirically confirmed: `ReadAngDataFilter-D3` (6.5.171 out-of-bounds ensemble write → SIGSEGV on non-contiguous phase indices; SIMPLNX resolved and pinned by test). **No SIMPLNX bugs** — the same latent OOB existed in the NX port and was found by the algorithm review and fixed before the comparison. | -| V&V phase | Historical status and sign-off retained. Section 4.3 adds real-HDF5 boundary verification and paired CTest evidence. | +| Aspect | Current state | +|--------|---------------| +| Algorithm Relationship | Minor changes to `ReadAngData` in DREAM.3D v6.5.171. SIMPLNX retains the import rules with changes to names, units, validation and array writes. | +| Oracle (confirmed) | Class 1 values and Class 4 ensemble invariants use inline fixtures in `test/ReadAngDataTest.cpp`. All pass in CI. | +| Code paths enumerated | 13 of 29 exercised by the ordinary CI cases. Gaps include defensive checks, cancellation, full-block writes and injected write failures. | +| Tests today | 9 test cases: 2 analytical/invariant cases, 5 error cases, 1 hidden block-boundary case and 1 SIMPL conversion case. | +| Exemplar archive | None in use. `read_ang_test.tar.gz` was retired and replaced by inline fixtures. | +| Legacy comparison | Not run on the current algorithm; the DREAM.3D v6.5.171 comparison dated 2026-07-07 predates later algorithm changes. The separate local proof run is dated 2026-09-17. | +| Bug flags | None | +| V&V phase | COMPLETE. The analytical fixtures and error cases pass in CI. | ## Summary -`ReadAngDataFilter` ("Read EDAX EBSD Data (.ang)") imports a single EDAX TSL `.ang` file into a new Image Geometry: it builds the geometry from the header (dims/step, z=1, origin 0, Micrometer), creates one cell array per data column plus the condensed 3-component `EulerAngles` and remapped `Phases` arrays, and populates the ensemble arrays (`CrystalStructures` via EbsdLib's symmetry mapping, trimmed `MaterialName`, `LatticeConstants`) with slot 0 reserved for the "Invalid Phase". Verification is Class 1 analytical + Class 4 invariant on a hand-authored inline toy `.ang` whose expected outputs were fully hand-derived (EbsdLib parsing itself is trusted upstream and not re-tested). Headline result: SIMPLNX matches the oracle exactly; against DREAM3D 6.5.171 all numeric outputs are bit-identical on supported files, with 4 documented deviations — including one empirically confirmed legacy crash bug (D3) whose latent NX twin was found and resolved. All 7 unit tests pass; the circular-oracle exemplar archive `read_ang_test.tar.gz` is retired. +`ReadAngDataFilter` imports one EDAX `.ang` scan into an Image Geometry with cell and ensemble arrays. Hand-derived values and ensemble invariants check geometry, phase remapping, Euler interleaving and copied data. The V&V fixed two bounds defects, and the ordinary tests pass in CI. ## Algorithm Relationship -*Classification:* **Minor changes.** +*Classification:* Minor changes -*Evidence:* Same SIMPL UUID inherited (`b8e128a8-…` → SIMPLNX `5b062816-…`) with `FromSIMPLJson` conversion and 6.4/6.5 fixtures at `test/simpl_conversion/{6_4,6_5}/ReadAngDataFilter.json`. The legacy `ReadAngData::copyRawEbsdData()` (705-line `.cpp`) is preserved essentially line-for-line in `Algorithms/ReadAngData.cpp` (phase remap loop, Euler interleave, verbatim column copies), and `loadMaterialInfo()` keeps the same slot-0-defaults + per-phase-fill structure. - -### Port-time deltas (each mapped to a Deviation entry where user-visible) - -1. **Material-name trim added** (D1). NX applies `StringUtilities::trimmed()`; legacy stores EbsdLib's raw token-rejoin which carries a trailing space. Changes output (string arrays only). -2. **TEM/ACOM units detection dropped** (D2). Legacy scans the header for `# TEM data` / ACOM markers and sets Nanometer units; NX hard-codes Micrometer. Deliberately not restored — EDAX retired those file variants 10+ years ago. Changes output metadata only for obsolete files. -3. **Non-contiguous phase-index handling** (D3, resolved). Preflight now sizes ensemble arrays by `maxPhaseIndex + 1` (identical to `phases.size() + 1` for well-formed files), `loadMaterialInfo` initializes every slot to Invalid-Phase defaults and guards the write range (`-19502`). Legacy has an out-of-bounds write here (segfault, demonstrated). -4. **`determineLaueGroup()` → `determineOrientationOpsIndex()`** — the EbsdLib symmetry→structure mapping was diffed function-body-for-function-body: **byte-identical** (pure rename). No output effect. -5. **Error-code renumbering** (D4) and framework-level file validation (extension/existence moved to `FileSystemPathParameter`). Rejection paths only. -6. **Legacy PIMPL file-cache dropped** (`ReadAngDataPrivate`/`Ang_Private_Data` — ported as dead declarations, removed). No output effect; NX re-reads the file on each execute. - -*Material PRs since baseline:* none identified for this filter beyond cross-cutting EbsdLib version bumps (the `ebsdlib` namespace migration visible in the current source); the May-2026 touch (`FromSIMPLJson`/conversion fixture refresh) does not alter the algorithm. +*Evidence:* `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/OrientationAnalysisLegacyUUIDMapping.hpp` maps `ReadAngData` to this filter; both sources retain phase remapping, Euler interleaving and ensemble population, with the documented validation and metadata changes. ## Oracle -*Class:* **1 (Analytical) + 4 (Invariant)**; EbsdLib parsing = **Class 2 boundary (trusted, not re-tested)**. - -### The EbsdLib boundary (what we do NOT re-test) - -EbsdLib's `AngReader` owns: header parsing (keys, phase sections, colon-chopping), data-column parsing, square-grid order fix-up, hex-grid rejection at read time, and its own error codes (`-150` no phases, `-600` truncated data, etc.). Those behaviors are upstream's to verify. The filter's value-add — everything this oracle covers — is the deterministic plumbing on top: geometry construction, array creation/typing, phase `<1 → 1` remap, Euler interleave, ensemble sizing + slot-0 defaults, symmetry-index placement, material-name trim, lattice-constant copy, and the value-add error paths (`-19500` HexGrid at preflight, `-19501` missing GRID, `-19502` phase-index range). - -### Applied +*Class:* 1 (Analytical), with 4 (Invariant) companions. -A hand-authored toy `.ang` (3 cols × 2 rows, XSTEP 0.25 / YSTEP 0.5, Cubic Nickel Symmetry 43 + Hexagonal "Titanium (Alpha)" Symmetry 62, points 2 and 5 carrying Phase 0) lives as a string literal in the test source; every fixture value is a multiple of 1/8 so all float32 comparisons are exact. Expected outputs — geometry (3,2,1)/(0.25,0.5,1.0)/(0,0,0)/Micrometer, `Phases {1,2,1,1,2,1}` (remap), the 18-value Euler interleave, 6 verbatim pass-through columns, `CrystalStructures {999,1,0}` (Unknown / Cubic_High / Hexagonal_High), trimmed material names, lattice constants — were derived by hand from the fixture text and the documented TSL symmetry codes, never by running any DREAM3D version. Class 4 invariants: ensemble tuple count = maxPhaseIndex+1, slot-0 (and any uncovered slot's) Invalid-Phase defaults. +*Applied:* Derive geometry and array values from a 3×2 two-phase ANG fixture; derive unused ensemble-slot defaults from a sparse phase-2 fixture. -*Encoded:* -- `test/ReadAngDataTest.cpp::"OrientationAnalysis::ReadAngDataFilter: Class 1 Analytical Oracle"` — the full Class 1 + Class 4 assertion set (geometry, 8 cell arrays element-wise, 3 ensemble arrays element-wise). -- `…::"Non-Contiguous Phase Index"` — Class 4 sizing/defaults invariant on the sparse-phase fixture (regression pin for D3). -- `…::"HexGrid Preflight Error (-19500)"`, `…::"Missing GRID Preflight Error (-19501)"` — value-add error paths. -- `…::"EbsdLib Error Passthrough - No Phase (-150)"`, `…::"EbsdLib Error Passthrough - Truncated Data (-600)"` — error propagation from the trusted boundary. -- `…::"SIMPL Backwards Compatibility"` — UUID + argument conversion (6.4 and 6.5 fixtures). +*Encoded:* `src/Plugins/OrientationAnalysis/test/ReadAngDataTest.cpp::OrientationAnalysis::ReadAngDataFilter: Class 1 Analytical Oracle` and `OrientationAnalysis::ReadAngDataFilter: Non-Contiguous Phase Index` — 2 inline fixtures. All pass in CI. -All 7 pass in the in-core `simplnx-rel` build. Reconciliation found zero SIMPLNX-vs-oracle discrepancies. - -*Second-engineer review:* skipped — documented reason: the filter's value-add is pure data plumbing with no numerical algorithm content; every oracle value is mechanically derivable from the fixture text (grid math, verbatim copies, a documented enum mapping), leaving no design freedom for the author-bias failure mode the review guards against. See `vv/provenance/read_ang_test.md`. - -## Algorithm review - -Line-by-line review performed via the `review-algorithm` skill after oracle reconciliation. All findings applied (all 7 tests still pass after rebuild): - -- **Robustness (Critical):** fixed the latent out-of-bounds ensemble write on non-contiguous phase indices (preflight `maxPhaseIndex+1` sizing; full default initialization of every slot; `-19502`/`-19504` phase-range guards in `loadMaterialInfo`). Legacy twin of this bug segfaults (Deviation D3). -- **Robustness:** added a cell-count guard at the top of `copyRawEbsdData` (`-19503`). The geometry is sized in preflight from the header's `NCOLS_EVEN`, but the reader allocates its buffers from `NCOLS_ODD`; the copies read `totalCells` elements from those buffers, so a file that changed between preflight and execute (or a malformed `NCOLS_EVEN > NCOLS_ODD` header) would read past the reader's heap. The guard rejects that case instead of reading out of bounds. Found by the adversarial review. -- **Dead code:** removed the vestigial legacy PIMPL (`ReadAngDataPrivate`, `Ang_Private_Data`, `m_AngDataPrivate` member, forward declaration), the never-firing `replace("MaterialName", "")`, unused `tDims`/`cDims` locals, the unused `FloatVec3Type` alias, and scaffold-generator boilerplate comments. -- **API consistency:** `loadMaterialInfo` now returns `Result<>` instead of `std::pair`. -- **Progress messaging:** added status messages before the EbsdLib read and the cell-data copy (the loops themselves are memcpy-speed; no throttled messenger warranted). -- **Naming:** local `CamelCase` path variables renamed to `camelBack`; doc `@brief`s filled in. +*Second-engineer review:* skipped — the recorded reason is that expected values follow directly from header values, column copies and symmetry mappings. ## Bugs found and fixed -No new defect was found during this storage recertification. Existing fixes and deviation dispositions remain documented above and in the sidecar. +All defects in this table are fixed. DREAM3D-NX v7.4.2 contains the fixes for `ReadAngDataFilter-D3` and `ReadAngDataFilter-D6`. + +| Deviation | Defect | Affected released versions | Resolution | +|-----------|--------|----------------------------|------------| +| `ReadAngDataFilter-D3` | Sparse phase indices could write outside the ensemble arrays. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | Size arrays to `max(maxPhaseIndex, phaseCount) + 1`, initialize all slots, and reject indices outside the allocated range. | +| `ReadAngDataFilter-D6` | A geometry larger than the reader buffers caused out-of-bounds reads. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected by inconsistent column counts. | Return `-19503` before copying when the reader has fewer points than the geometry. | ## Code path coverage -*14 of 18 enumerated paths exercised; 1 is unreachable dead code (row 8) and 3 are file-changed/malformed-input guards or cancel checks that need injection (rows 11b, 15b, 16). Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ReadAngData.cpp` + preflight in `Filters/ReadAngDataFilter.cpp`.* Logical phases: **(a)** preflight (header-only read → output actions), **(b)** execute read + ensemble population (`loadMaterialInfo`), **(c)** cell-data copy (`copyRawEbsdData`). - -| # | Phase | Path | Test case | -|----|-------|------|-----------| -| 1 | (a) Preflight | `readHeaderOnly` error → passthrough error | *Not directly tested as a distinct case — `FileSystemPathParameter` rejects missing files first; unreadable-header content surfaces via the same return as paths 2/3.* Exercised implicitly by every error fixture. | -| 2 | (a) Preflight | `getGrid()` empty → `-19501` | `Missing GRID Preflight Error (-19501)` | -| 3 | (a) Preflight | `getGrid() == "HexGrid"` → `-19500` (after actions are built, so scan info still reaches the UI) | `HexGrid Preflight Error (-19500)` | -| 4 | (a) Preflight | Geometry action: dims (X,Y,1), spacing (XStEP,YSTEP,1), origin (0,0,0), Micrometer | `Class 1 Analytical Oracle` | -| 5 | (a) Preflight | Per-column array creation with int32/float32 type dispatch from `AngFields` | `Class 1 Analytical Oracle` (all 8 cell arrays exist with right types) | -| 6 | (a) Preflight | Ensemble AM sized `maxPhaseIndex + 1`; CrystalStructures/LatticeConstants/MaterialName actions | `Class 1 Analytical Oracle` (3 tuples) + `Non-Contiguous Phase Index` (3 tuples from max index 2) | -| 7 | (b) Execute | `readFile()` error → passthrough | `EbsdLib Error Passthrough - No Phase (-150)` *(−110/−100 variants are the same return statement)* and `…Truncated Data (-600)` | -| 8 | (b) Execute | `phases.empty()` → error passthrough | *Unreachable dead code: `AngReader::readFile()` returns `-150` when the phase vector is empty, so `operator()` returns at the readFile error check (row 7) before `loadMaterialInfo` is ever entered with empty phases.* | -| 9 | (b) Execute | All-slot Invalid-Phase default initialization | `Non-Contiguous Phase Index` (slot 1 keeps defaults) + `Class 1 Analytical Oracle` (slot 0) | -| 10 | (b) Execute | Per-phase fill: symmetry→structure index, trimmed name, lattice constants | `Class 1 Analytical Oracle` (2 phases, 2 Laue classes) | -| 11a | (b) Execute | `phaseID < 1` (Phase 0 / negative) → `-19502` | `Phase 0 rejected (-19502)` — static `# Phase 0` fixture, trips deterministically (D5) | -| 11b | (b) Execute | `phaseID >= ensemble count` → `-19504` | *Not directly tested. Only reachable if the file changes between preflight and execute; requires file-mutation injection.* | -| 12 | (c) Copy | Phase remap `< 1 → 1` then copy | `Class 1 Analytical Oracle` (points 2, 5) | -| 13 | (c) Copy | Euler interleave `3i/3i+1/3i+2` | `Class 1 Analytical Oracle` (18 values) | -| 14 | (c) Copy | Verbatim copies: IQ, CI, SEM Signal, Fit, X/Y Position | `Class 1 Analytical Oracle` (6 arrays element-wise) | -| 15b | (c) Copy | reader element count `< totalCells` → `-19503` (out-of-bounds read guard) | *Not directly tested. Only reachable if the file changes between preflight and execute or a malformed `NCOLS_EVEN > NCOLS_ODD` header; requires injection.* | -| 16 | (b)/(c) | Cancel checks (4 sites) | *Not directly tested. Requires cancel-signal injection; standard early-return pattern.* | -| 18 | OOC boundary | 65536-block import oracle | `real HDF5 65536-block import oracle` — independent expected outputs on actual HDF5 stores | +13 of 29 paths exercised. The count uses the eight ordinary CI cases; each remaining row states its coverage gap. + +Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ReadAngData.cpp` (246 lines) and `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ReadAngDataFilter.cpp` (258 lines). + +Test names below omit the common `OrientationAnalysis::ReadAngDataFilter: ` prefix. + +| # | Phase | Path | Test case | +|---|-------|------|-----------| +| 1 | Preflight | Propagate a header-reader error | *Not directly tested. No fixture makes `readHeaderOnly()` fail.* | +| 2 | Preflight | Reject a missing GRID key with `-19501` | `Missing GRID Preflight Error (-19501)` | +| 3 | Preflight | Reject HexGrid with `-19500` | `HexGrid Preflight Error (-19500)` | +| 4 | Preflight | Create geometry with header dimensions and spacing, zero origin and Micrometer units | `Class 1 Analytical Oracle` | +| 5 | Preflight | Create float32 arrays for the six ANG data columns | `Class 1 Analytical Oracle` | +| 6 | Preflight | Create an int32 array in the column-dispatch loop | *Not directly tested. `AngFields::getFilterFeatures()` supplies only float32 fields.* | +| 7 | Preflight | Skip a column with neither supported type | *Not directly tested. The fixed ANG field list supplies only supported float32 fields.* | +| 8 | Preflight | Create int32 Phases and three-component float32 EulerAngles | `Class 1 Analytical Oracle` | +| 9 | Preflight | Size ensemble arrays to `max(maxPhaseIndex, phaseCount) + 1` | `Class 1 Analytical Oracle`; `Non-Contiguous Phase Index` | +| 10 | Read | Propagate `readFile()` errors | `EbsdLib Error Passthrough - No Phase (-150)`; `EbsdLib Error Passthrough - Truncated Data (-600)` | +| 11 | Ensemble | Reject an empty phase vector in `loadMaterialInfo()` | *Not directly tested. The reader returns `-150` before this helper can receive an empty vector.* | +| 12 | Ensemble | Initialize slot 0 and unused slots to invalid-phase defaults | `Class 1 Analytical Oracle`; `Non-Contiguous Phase Index` | +| 13 | Ensemble | Copy symmetry indices, trimmed material names and lattice constants | `Class 1 Analytical Oracle`; `Non-Contiguous Phase Index` | +| 14 | Ensemble | Reject a phase section with index below 1 using `-19502` | `Phase 0 rejected (-19502)` | +| 15 | Ensemble | Reject an index at or above the allocated count using `-19504` | *Not directly tested. Requires a file change between preflight and execution.* | +| 16 | Copy | Reject fewer reader points than geometry cells using `-19503` | *Not directly tested. No fixture changes the file or supplies inconsistent column counts.* | +| 17 | Copy | Map cell phases below 1 to 1; preserve positive phases | `Class 1 Analytical Oracle` | +| 18 | Copy | Interleave and write a partial Euler block | `Class 1 Analytical Oracle` | +| 19 | Copy | Write a full 65,536-tuple Euler block and a tail | *Not directly tested. Ordinary CI fixtures have six points; the separate hidden `real HDF5 65536-block import oracle` defines this boundary fixture.* | +| 20 | Copy | Copy Image Quality, Confidence Index, SEM Signal, Fit and X/Y Position | `Class 1 Analytical Oracle` | +| 21 | Cancellation | Return at any of the four cancellation checks | *Not directly tested. Requires cancellation injection.* | +| 22 | Write | Propagate a Phases write failure | *Not directly tested. Requires an injected store write failure.* | +| 23 | Write | Propagate an EulerAngles write failure | *Not directly tested. Requires an injected store write failure.* | +| 24 | Write | Propagate an Image Quality write failure | *Not directly tested. Requires an injected store write failure.* | +| 25 | Write | Propagate a Confidence Index write failure | *Not directly tested. Requires an injected store write failure.* | +| 26 | Write | Propagate a SEM Signal write failure | *Not directly tested. Requires an injected store write failure.* | +| 27 | Write | Propagate a Fit write failure | *Not directly tested. Requires an injected store write failure.* | +| 28 | Write | Propagate an X Position write failure | *Not directly tested. Requires an injected store write failure.* | +| 29 | Write | Propagate a Y Position write failure | *Not directly tested. Requires an injected store write failure.* | ## Test inventory +The eight ordinary cases are in `src/Plugins/OrientationAnalysis/test/ReadAngDataTest.cpp`. All pass in CI. The same file contains the hidden boundary case listed below. + | Test case | Status | Notes | |-----------|--------|-------| -| `OrientationAnalysis::ReadAngDataFilter: Class 1 Analytical Oracle` | new-for-V&V | Class 1 + 4. Geometry (dims/spacing/origin/units), 8 cell arrays and 3 ensemble arrays asserted element-wise (≈70 assertions), all expected values hand-derived, float32-exact fixture. Inline data. | -| `OrientationAnalysis::ReadAngDataFilter: Non-Contiguous Phase Index` | new-for-V&V | Class 4. Sparse `# Phase 2`-only file → ensemble sized to 3 tuples, uncovered slot keeps Invalid-Phase defaults. Regression pin for the OOB fix (Deviation D3). | -| `OrientationAnalysis::ReadAngDataFilter: Phase 0 rejected (-19502)` | new-for-V&V | Value-add execute rejection. Static `# Phase 0` fixture; asserts `-19502`. Pins deviation D5 (legacy accepted Phase 0) and refutes the earlier claim that the phase-range guard needs file-mutation injection to reach. | -| `OrientationAnalysis::ReadAngDataFilter: HexGrid Preflight Error (-19500)` | new-for-V&V | Value-add preflight rejection. | -| `OrientationAnalysis::ReadAngDataFilter: Missing GRID Preflight Error (-19501)` | new-for-V&V | Value-add preflight rejection. | -| `OrientationAnalysis::ReadAngDataFilter: EbsdLib Error Passthrough - No Phase (-150)` | new-for-V&V | Replaces the retired archive-based "Invalid Phase" test with an inline no-phase-header fixture; same error code asserted. | -| `OrientationAnalysis::ReadAngDataFilter: EbsdLib Error Passthrough - Truncated Data (-600)` | new-for-V&V | Replaces the retired archive-based "Invalid Columns & Rows" test with an inline truncated-data fixture; same error code asserted. | -| `OrientationAnalysis::ReadAngDataFilter: SIMPL Backwards Compatibility` | new-for-V&V | `DYNAMIC_SECTION` over the existing 6.4/6.5 conversion fixtures — the filter previously had **no** conversion test despite shipping the fixtures. | -| *(retired)* `OrientationAnalysis::ReadAngData: Exemplary Test` | retired | Exemplar comparison against `read_ang_test.dream3d` — **circular oracle** (exemplar generated from this filter's own output). Replaced by the Class 1 oracle above. | -| *(retired)* `OrientationAnalysis::ReadAngData: Invalid Phase` | retired | Archive-based `-150` test; superseded by the inline passthrough test. | -| *(retired)* `OrientationAnalysis::ReadAngData: Invalid Columns & Rows` | retired | Archive-based `-600` test; superseded by the inline passthrough test. | -| `real HDF5 65536-block import oracle` | new-for-V&V | 36 assertions; A generated 65,537-point ANG file puts distinct exact Euler triples and Image Quality values at the end of the full interleaving block and in its one-point tail. Phase zero in the tail must map to phase one, preserving the established ANG contract. The three checked output arrays are HDF5-OOC. | -| `Phase 0 rejected (-19502)` | kept | Existing invalid phase-header regression, distinct from remapping unindexed cell values. | - -OOC recertification (2026-09-21): serial CTest passed 8/8 target entries in each DREAM3DNX build. The hidden `real HDF5 65536-block import oracle` passed 36 assertions in the OOC binary. A generated 65,537-point ANG file puts distinct exact Euler triples and Image Quality values at the end of the full interleaving block and in its one-point tail. Phase zero in the tail must map to phase one, preserving the established ANG contract. The three checked output arrays are HDF5-OOC. The new case is included in the plugin's OOC store-contract CTest group. Upstream oracle assertions and tolerances remain intact. No new legacy binary comparison is claimed. +| `OrientationAnalysis::ReadAngDataFilter: Class 1 Analytical Oracle` | new-for-V&V | Checks geometry, all eight cell arrays, all three ensemble arrays and inherited tuple dimensions against inline expected values. | +| `OrientationAnalysis::ReadAngDataFilter: Non-Contiguous Phase Index` | new-for-V&V | A phase-2-only header requires three ensemble tuples; slots 0 and 1 retain invalid-phase defaults. Covers D3. | +| `OrientationAnalysis::ReadAngDataFilter: HexGrid Preflight Error (-19500)` | new-for-V&V | Checks preflight error `-19500` for HexGrid. | +| `OrientationAnalysis::ReadAngDataFilter: Missing GRID Preflight Error (-19501)` | new-for-V&V | Checks preflight error `-19501` when GRID is absent. | +| `OrientationAnalysis::ReadAngDataFilter: EbsdLib Error Passthrough - No Phase (-150)` | new-for-V&V | Checks successful preflight and execution error `-150` for a header without phases. | +| `OrientationAnalysis::ReadAngDataFilter: Phase 0 rejected (-19502)` | new-for-V&V | Checks execution error `-19502` for a phase-0 header section. Covers D5. | +| `OrientationAnalysis::ReadAngDataFilter: EbsdLib Error Passthrough - Truncated Data (-600)` | new-for-V&V | Checks execution error `-600` when four data rows are supplied for six points. | +| `OrientationAnalysis::ReadAngDataFilter: real HDF5 65536-block import oracle` | new-for-V&V | Hidden. Defines 65,537 points and checks EulerAngles, Phases and Image Quality at the full-block boundary and tail, plus geometry and tuple dimensions. | +| `OrientationAnalysis::ReadAngDataFilter: SIMPL Backwards Compatibility` | new-for-V&V | The `SIMPL 6.5 (UUID)` and `SIMPL 6.4 (Filter_Name)` dynamic sections check filter identity, empty comments and all four converted arguments. | ## Exemplar archive -- **Archive:** None. **`read_ang_test.tar.gz` retired** (SHA512 was `de7cd89d…e4b236`); its `download_test_data()` entry is removed from `test/CMakeLists.txt`. -- **Provenance:** `src/Plugins/OrientationAnalysis/vv/provenance/read_ang_test.md` — documents the circular-oracle status and the inline replacement. -- All current oracle data is inline in `test/ReadAngDataTest.cpp`; there is nothing to archive. +- **Archive:** None in use; `read_ang_test.tar.gz` is retired. Current fixtures are inline. +- **SHA512:** No current registration in `src/Plugins/OrientationAnalysis/test/CMakeLists.txt`; the retired registration was `de7cd89d925da01f291f44686964ec89d469659d0005219f9869afe26b8f62af278461ac3f5deb3afe7f3e65ec074ab3a1357d77a1a5f92eb3a1ea8cc5e4b236`. +- **Provenance:** `src/Plugins/OrientationAnalysis/vv/provenance/ReadAngDataFilter.md` -## Deviations from DREAM3D 6.5.171 +## Deviations from DREAM.3D v6.5.171 -*Comparison run 2026-07-07 against the official DREAM3D 6.5.171 release on three byte-identical-input fixtures (toy `.ang`, Small IN100 `Slice_1.ang`, sparse-phase toy). All numeric outputs bit-identical on the supported-format fixtures. Full write-ups: `vv/deviations/ReadAngDataFilter.md`; working artifacts in `Code_Review/ReadAngDataFilter/`.* +See `vv/deviations/ReadAngDataFilter.md` for the root cause, affected users and recommendation. This table lists only Part 1 entries. -- `ReadAngDataFilter-D1` — MaterialName trailing space: legacy stores `"Nickel "`, SIMPLNX trims to `"Nickel"`. Demonstrated on both fixtures. Trust SIMPLNX. -- `ReadAngDataFilter-D2` — TEM/ACOM `.ang` variants get Nanometer units in legacy, Micrometer in SIMPLNX. Document-only: EDAX retired those files 10+ years ago. Either acceptable. -- `ReadAngDataFilter-D3` — **Legacy crash bug, empirically confirmed:** non-contiguous phase indices segfault 6.5.171 (OOB ensemble write, exit 139); SIMPLNX imports correctly (resolved, test-pinned). Trust SIMPLNX. -- `ReadAngDataFilter-D4` — Error-code renumbering on rejection paths (`-1000`→`-19500` HexGrid, etc.). No data effect. Either acceptable. -- `ReadAngDataFilter-D5` — A `# Phase 0` section is accepted by 6.5.171 but rejected by SIMPLNX at execute (`-19502`); `.ang` phase numbering starts at 1. Pinned by a static fixture. Trust SIMPLNX. +| Deviation | Observed difference | +|-----------|---------------------| +| `ReadAngDataFilter-D1` | SIMPLNX trims material names; DREAM.3D v6.5.171 preserves trailing spaces. | +| `ReadAngDataFilter-D2` | SIMPLNX assigns Micrometer units to TEM/ACOM headers that DREAM.3D v6.5.171 assigns Nanometer units. | +| `ReadAngDataFilter-D3` | SIMPLNX imports sparse phase indices with valid ensemble bounds; DREAM.3D v6.5.171 can crash. | +| `ReadAngDataFilter-D4` | Rejected files can have different error codes and detection stages. | +| `ReadAngDataFilter-D5` | SIMPLNX rejects phase-0 header sections; DREAM.3D v6.5.171 writes them into ensemble slot 0. | diff --git a/src/Plugins/OrientationAnalysis/vv/ReadCtfDataFilter.md b/src/Plugins/OrientationAnalysis/vv/ReadCtfDataFilter.md index f0d2bf7a44..23430305a7 100644 --- a/src/Plugins/OrientationAnalysis/vv/ReadCtfDataFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/ReadCtfDataFilter.md @@ -1,163 +1,137 @@ # V&V Report: ReadCtfDataFilter -| | | -|-----------|--------------------------| -| Plugin | OrientationAnalysis | +| | | +|--------|--------------| +| Plugin | OrientationAnalysis | | SIMPLNX UUID | `7751923c-afb9-4032-8372-8078325c69a4` | | SIMPLNX Human Name | Read Oxford Instr. EBSD Data (.ctf) | -| DREAM3D 6.5.171 equivalent | `ReadCtfData` (SIMPL UUID `d1df969c-0428-53c3-b61d-99ea2bb6da28`) — `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/ReadCtfData.{h,cpp}` | +| DREAM.3D v6.5.171 equivalent | `ReadCtfData` (SIMPL UUID `d1df969c-0428-53c3-b61d-99ea2bb6da28`) | | Verified commit | ** | | Status | COMPLETE | -| Sign-off | Michael A. Jackson — 2026-07-24. Second engineer: Jared Duffey, 2026-07-28 (PR #1692 review). | +| Sign-off | Michael Jackson, 2026-07-24; second engineer: Jared Duffey, 2026-07-28 | ## At a glance -| Aspect | Current state | -|------------------------|--------------------------| -| Algorithm Relationship | **Minor changes.** Faithful port of legacy `ReadCtfData` control flow with deliberate deltas: unindexed-point (Phase 0) remap removed (D1/D2, PR #937 — predates this pass), 3D multi-slice support restored (D4, resolved), five malformed-input guards added (D3), Euler math on double-precision intermediates (restores legacy bit-parity), and the legacy PIMPL file-cache dropped (no output effect). | -| Oracle (confirmed) | **Confirmed.** **Class 1 (analytical) + Class 4 (invariant)**, scoped to the filter's value-add per the "don't re-test upstream" rule — EbsdLib (vcpkg 3.1.0) owns `.ctf` parsing and is trusted (Class 2 boundary). Hand-authored inline toy `.ctf` fixtures (3×2 two-phase, 2×2×2 multi-slice; all values float32-exact) with every expected value hand-derived from the fixture text; the two angle transforms (+30° hex alignment, degrees→radians) are correctly-rounded IEEE-754 double-intermediate results derived independently with NumPy. Encoded as 12 TEST_CASEs in `test/ReadCtfDataTest.cpp`; all pass. SIMPLNX matched the oracle with zero discrepancies. | -| Code paths enumerated | 20 of 23 paths exercised (see Code path coverage); the gaps are the unreadable-header passthrough (needs permission manipulation), the file-changed phase-count guard `-19605` (race window inside a single execute; needs injection), and the cancel-signal paths (need injection; untested per scope). | -| Tests today | 12 registered cases plus one hidden HDF5 boundary test. Paired CTest selections pass 12/12. | -| Exemplar archive | **None — retired `6_6_read_ctf_data_2.tar.gz`** (legacy-generated exemplar = forbidden oracle). `download_test_data()` entry removed from `test/CMakeLists.txt`; retirement documented in `vv/provenance/6_6_read_ctf_data_2.md`. Its production Cugrid scan lives on as an A/B fixture in the comparison working folder only. | -| Legacy comparison | **Run (2026-07-24) vs the official DREAM3D 6.5.171 release.** Four runs over three byte-identical input files: toy (2 conversion combos), Cugrid 550×400 production scan, 2×2×2 multi-slice toy. **All numeric outputs bit-identical** — 543,950 of the production scan's 660,000 Euler values match exactly (the 116,050 differing values are all the unindexed points' φ2), and the toy's double-precision-pinning angles match legacy bit-for-bit — except the unindexed-point family: Phases 0→1 remap (D1) and the consequent +30° on unindexed φ2 (D2). Three malformed-input fixtures demonstrated legacy segfaults/silent corruption vs SIMPLNX errors (D3). | -| Bug flags | Legacy: crash/UB on malformed files, **empirically confirmed** (two segfaults, one silent heap-dependent output) — D3. SIMPLNX (all releases through 7.4.1): multi-slice `.ctf` silently truncated to slice 0 — D4, **resolved** and pinned by the 3D test; latent OOB/null-deref twins of the legacy crashes existed in the NX copy path and were guarded this pass (−19600/−19601/−19602/−19603). | -| V&V phase | Historical status and sign-off retained. Section 4.3 adds real-HDF5 boundary verification and paired CTest evidence. | +| Aspect | Current state | +|--------|---------------| +| Algorithm Relationship | Minor changes to `ReadCtfData` in DREAM.3D v6.5.171. SIMPLNX preserves phase zero, validates malformed input, and aligns both hexagonal Laue classes. | +| Oracle (confirmed) | Classes 1 and 4 use hand-authored CTF fixtures and exact expected values in `test/ReadCtfDataTest.cpp`. All pass in CI. | +| Code paths enumerated | 18 of 33 exercised by the default test cases. Remaining paths include defensive guards, cancellation, display text, large-block processing, and I/O failures. | +| Tests today | 13 test cases: 3 analytical cases, 8 error cases, 1 SIMPL conversion case, and 1 hidden block-boundary case. The 12 default cases cover geometry, arrays, conversions, and input errors. | +| Exemplar archive | None. The retired `6_6_read_ctf_data_2.tar.gz` was replaced by inline analytical fixtures. | +| Legacy comparison | Not run on the current algorithm. The DREAM.3D v6.5.171 comparison dated 2026-07-24 predates the current hexagonal-alignment correction. | +| Bug flags | None | +| V&V phase | COMPLETE. All default tests pass in CI. | ## Summary -`ReadCtfDataFilter` ("Read Oxford Instr. EBSD Data (.ctf)") imports a single Oxford/HKL Channel 5 `.ctf` file into a new Image Geometry: it builds the geometry from the header (XCells/YCells/ZCells, XStep/YStep/ZStep, origin 0, Micrometer), creates one cell array per data column plus the condensed 3-component `EulerAngles` and verbatim `Phases` arrays (optionally applying the EDAX hexagonal +30° φ2 alignment and degrees→radians conversion), and populates the ensemble arrays (`CrystalStructures` via EbsdLib's Laue-group mapping, `MaterialName`, `LatticeConstants`) with slot 0 reserved for the "Invalid Phase". Verification is Class 1 analytical + Class 4 invariant on hand-authored inline toy `.ctf` fixtures whose expected outputs were derived independently of both codebases (EbsdLib parsing is trusted upstream and not re-tested). Headline result: SIMPLNX matches the oracle exactly; against DREAM3D 6.5.171 every numeric output is bit-identical on all four fixtures except the deliberate unindexed-point deviations (D1/D2), with one SIMPLNX functional gap (3D multi-slice import, D4) found and resolved and legacy's malformed-input crashes (D3) empirically demonstrated. All 12 unit tests pass; the legacy-oracle exemplar archive `6_6_read_ctf_data_2.tar.gz` is retired. +`ReadCtfDataFilter` imports an Oxford/HKL CTF file into an Image Geometry with cell and phase data. Classes 1 and 4 check geometry, copied arrays, phase-zero preservation, and all four angle-conversion modes. All pass in CI. ## Algorithm Relationship -*Classification:* **Minor changes.** +*Classification:* Minor changes -*Evidence:* Same SIMPL UUID inherited (`d1df969c-…` → SIMPLNX `7751923c-…`) with `FromSIMPLJson` conversion and 6.4/6.5 fixtures at `test/simpl_conversion/{6_4,6_5}/ReadCtfDataFilter.json`. The legacy `ReadCtfData::copyRawEbsdData()` structure (phase copy, Euler interleave with hex-alignment/radians options, verbatim column copies) and `loadMaterialInfo()` (slot-0 defaults + per-phase fill) are preserved block-for-block in `Algorithms/ReadCtfData.cpp`. - -### Port-time deltas (each mapped to a Deviation entry where user-visible) - -1. **Unindexed-point remap removed** (D1, and consequence D2). Legacy remaps phase `< 1 → 1` in the reader buffer before storing `Phases`, which also routes unindexed points into the hex +30° branch; SIMPLNX copies the phase column verbatim (deliberate change, PR #937, May 2024). Changes `Phases` and unindexed-point `EulerAngles` output. -2. **3D multi-slice support restored** (D4, resolved). Legacy sizes the geometry `XCells × YCells × ZCells` with `ZStep` slice thickness; the NX port had hard-coded z=1/1.0 and silently imported only slice 0. Preflight now mirrors legacy (`ZCells` clamped to ≥1; `ZStep` of 0 → 1.0). Bit-identical to legacy on the 3D fixture. -3. **Malformed-input guards added** (D3): empty phase list (−19600), missing data column (−19601), out-of-range phase value (−19602), reader/geometry cell-count mismatch (−19603), zero XCells/YCells at preflight (−19604), and a file-changed phase-count guard in the ensemble fill (−19605, the ReadAngData −19504 analog). Legacy segfaults or silently produces heap-dependent output on these inputs (demonstrated where statically reachable). Rejection paths only; no effect on well-formed files. -4. **Double-precision Euler intermediates.** Legacy computes `+30.0` and `× M_PI/180.0` with double intermediates (float operands promoted); the NX port used float32 arithmetic throughout. Restored double intermediates this pass — more accurate (correctly-rounded float32 results) and bit-identical to legacy on all indexed points of every fixture. No deviation entry needed. -5. **`determineLaueGroup()` → `determineOrientationOpsIndex()`** — the EbsdLib Laue-group→structure mapping was diffed function-body-for-function-body against the 6.5.171 bundled EbsdLib: **identical bodies and identical enum values** (pure rename). No output effect. -6. **Error-code source fix.** `CtfReader::readFile()` leaves its error-code member at 0 on its zero-step/zero-cells rejections (message only); the filter now falls back to the returned code (−102/−103) instead of reporting error 0. Rejection paths only. -7. **Legacy PIMPL file-cache dropped** (`ReadCtfDataPrivate`/`Ctf_Private_Data` — ported as dead declarations, removed). No output effect; NX re-reads the file on each execute. - -*Material PRs since baseline:* PR #937 (phase-0 remap removal, May 2024 — deviation D1/D2); EbsdLib API migrations (#1122, #1472 — `ebsdlib` namespace, no algorithm change). +*Evidence:* `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/OrientationAnalysisLegacyUUIDMapping.hpp` maps `ReadCtfData` to this filter; both use phase loading, Euler interleaving, and column copying, with the differences listed in the deviations sidecar. ## Oracle -*Class:* **1 (Analytical) + 4 (Invariant)**; EbsdLib parsing = **Class 2 boundary (trusted, not re-tested)**. - -### The EbsdLib boundary (what we do NOT re-test) - -EbsdLib's `CtfReader` (vcpkg 3.1.0) owns: header-key parsing, phase-section parsing (lattice constants/angles, names, Laue group), data-column tokenizing and typed buffer allocation, European-decimal fix-up, multi-slice line ordering, and its own error codes (−100 unopenable, −102/−103 zero step/cells, −105 premature EOF, −107 unknown column, etc.). Those behaviors are upstream's to verify. The filter's value-add — everything this oracle covers — is the deterministic plumbing on top: geometry construction (including 3D), array creation/typing, verbatim phase copy, Euler interleave + the two optional angle transforms, ensemble sizing + slot-0 defaults, Laue-group index placement, lattice-constant copy, and the six value-add error paths (−19600…−19605). - -### Applied - -Hand-authored toy `.ctf` fixtures live as string literals in the test source: a 3×2 grid (XStep 0.25 / YStep 0.5) with hexagonal "Hex Phase A" (Laue 9) + cubic "Copper" (Laue 11) and two unindexed Phase-0 points, and a 2×2×2 multi-slice variant (ZStep 0.75). Every fixture value is float32-exact (multiples of 1/8), so verbatim copies are asserted with exact equality. Expected outputs — geometry (3,2,1)/(0.25,0.5,1.0)/(0,0,0)/Micrometer and (2,2,2)/(0.25,0.5,0.75), `Phases {1,2,0,1,2,0}` (verbatim, Phase 0 preserved), the 18-value Euler interleave, 7 verbatim pass-through columns, `CrystalStructures {999,0,1}` (Unknown / Hexagonal_High / Cubic_High from documented HKL Laue groups 9 and 11), material names, lattice constants — were derived by hand from the fixture text. The +30° and degrees→radians expectations are the correctly-rounded float32 results of double-precision arithmetic (`value × M_PI/180.0`), computed independently with NumPy IEEE-754 float32/float64 semantics and embedded as exact literals with derivation comments. Class 4 invariants: ensemble tuple count = phases+1; slot-0 Invalid-Phase defaults; `Phases` unaffected by either conversion option; unindexed/cubic points never receive the hex shift. - -*Encoded:* -- `test/ReadCtfDataTest.cpp::"…Class 1 Analytical Oracle"` — full Class 1+4 assertion set (geometry, 9 cell arrays element-wise, 3 ensemble arrays element-wise). -- `…::"Euler Conversion Combinations"` — DYNAMIC_SECTION over the 2×2 DegreesToRadians × EdaxHexagonalAlignment grid, 18 Euler values each, hex shift scoped to Hexagonal_High points only. -- `…::"3D Multi-Slice CTF"` — DYNAMIC_SECTION over ZStep present/absent; dims/spacing/arrays for the 2-slice fixture (regression pin for D4). -- `…::"Empty Phases rejected (-19600)"`, `…::"Missing Data Column rejected (-19601)"`, `…::"Out-of-Range Phase Value rejected (-19602)"`, `…::"Reader/Geometry Cell Count Mismatch rejected (-19603)"`, `…::"Invalid Cells Preflight Error (-19604)"` — value-add guards. -- `…::"EbsdLib Error Passthrough - Zero Step (-102)"`, `…- Truncated Data (-105)"`, `…- Unknown Column (-107)"` — error propagation from the trusted boundary (three distinct return points in `CtfReader`). -- `…::"SIMPL Backwards Compatibility"` — UUID + argument conversion (6.4 and 6.5 fixtures; DYNAMIC_SECTION). +*Class:* 1 (Analytical) and 4 (Invariant). -All 12 pass in `simplnx-Rel` and `simplnx-ooc-Rel`. Reconciliation found zero SIMPLNX-vs-oracle discrepancies (the pre-identified fixes — 3D support, guards, double intermediates — were implemented before the first oracle run). Every fixed delta has a regression pin that fails against the pre-fix code: the guard error codes did not exist, the 3D dims assertion cannot pass against the z=1 hard-code, and — added after the adversarial review found the original angles blind to it — four Euler fixture values (7.125°, 3.375°, 6.75°, and 37.125° via the hex shift) whose correctly-rounded results differ between float32 arithmetic and the double-precision intermediates. +*Applied:* Hand-authored CTF text defines geometry and copied values; double-precision angle formulas define float32 expectations, and invariants check phase preservation and ensemble sizing. -*Second-engineer review:* **Signed off by Jared Duffey, 2026-07-28** (PR #1692 review). A dedicated oracle re-derivation was scoped as unnecessary — documented reason: the filter's value-add is pure data plumbing plus two elementwise transforms whose expected values are mechanically derivable IEEE-754 roundings, leaving no design freedom for the author-bias failure mode the review guards against. See `vv/provenance/6_6_read_ctf_data_2.md`. The V&V work was authored by Michael A. Jackson, so the PR review is independent of the author. +*Encoded:* `src/Plugins/OrientationAnalysis/test/ReadCtfDataTest.cpp` — `Class 1 Analytical Oracle`, `Euler Conversion Combinations`, and `3D Multi-Slice CTF` use three fixture layouts, including four conversion modes and two slice-spacing variants. All pass in CI. -## Algorithm review - -Line-by-line review performed via the `review-algorithm` skill after oracle reconciliation. All findings applied (all 12 tests pass after rebuild): - -- **Robustness (Critical):** added the five malformed-input guards (−19600…−19604) replacing two demonstrated-segfault paths, one silent OOB-read path, and two zero-dimension paths (details under D3). -- **Correctness:** Euler transforms moved to double-precision intermediates (correctly-rounded results; restores legacy bit-parity). Error-code fallback for `CtfReader` failure paths that set only the message. -- **Dead code:** removed the vestigial legacy PIMPL (`ReadCtfDataPrivate`, `Ctf_Private_Data`), the unused `FloatVec3Type` alias, unused `tDims`/`cDims` locals, a dead `PreflightResult` local, an unused `LaueOps.h` include, and scaffold-generator comments. -- **API consistency:** `loadMaterialInfo`/`copyRawEbsdData` now return `Result<>` (matches post-V&V `ReadAngData`). -- **Progress messaging:** status messages before the EbsdLib read and the cell-data copy (loops are memcpy-speed; no throttled messenger warranted). -- **Cancel checks:** 3 early-return sites added. -- **Naming:** `angPhases` → `ctfPhases`; doc `@brief`s filled in. - -### Five-perspective hardening review (post-deliverable) - -Five independent reviews were run after the deliverables were drafted; all findings applied and all 12 tests re-pass: - -- **Adversarial:** found the conversion suite blind to the double-precision Euler fix (all original angles round identically under float32 and double arithmetic) — fixed by re-deriving the fixture with four diverging angles, re-verified bit-identical against 6.5.171; found a stale −19600 rationale comment and a wrong "mirrors readData" claim for negative ZCells (negative ZCells made `CtfReader::readData()` read zero lines and surface a garbage-value −19602 — now rejected at preflight with −19604 and tested); moved the −19602 test to the exact boundary value; corrected two report wording errors. All hand-derived oracle values, guard-reachability claims, and A/B consistency survived independent recomputation. -- **Senior engineer:** added the −19605 file-changed phase-count guard (the ReadAngData −19504 analog), the preflight error-code fallback, removed dead `getCancel()`, include-what-you-use and sibling-parity cleanups, error-message substring pins and `CheckArraysInheritTupleDims` in every error test, docs grammar/markup fixes. -- **CPU performance:** no action needed — copies are ~5–10% of wall time (EbsdLib text parsing dominates); idioms match the post-V&V ReadAngData baseline; the only lever (bulk store I/O) does not exist on this branch. -- **Memory:** no bugs; peak ≈ 88 bytes/scan-point (reader + destination resident simultaneously) now documented in the filter docs; reader lifetime/cleanup verified correct on all return paths. -- **Out-of-core:** all destination writes are forward-sequential and chunk-cache-benign; the Euler loop's re-read of the just-written Phases array was replaced with the reader's in-core buffer (bit-identical, removes an OOC read-back stream); `copyFromBuffer`-style conversions deferred until that API exists outside the OOC rewrite branch. +*Second-engineer review:* Jared Duffey, 2026-07-28; separate oracle re-derivation skipped because the expectations follow direct copies and elementwise formulas. ## Bugs found and fixed -No new defect was found during this storage recertification. Existing fixes and deviation dispositions remain documented above and in the sidecar. +All defects in this table are fixed. DREAM3D-NX v7.4.2 contains the fixes for `ReadCtfDataFilter-D3`, `ReadCtfDataFilter-D4`, `ReadCtfDataFilter-D6`, and `ReadCtfDataFilter-D7`. The fix for `ReadCtfDataFilter-D5` will be in the DREAM3D-NX release after v7.4.2. + +| Deviation | Defect | Affected released versions | Resolution | +|-----------|--------|----------------------------|------------| +| `ReadCtfDataFilter-D3` | Missing columns or invalid phase data could cause invalid memory access or incorrect output. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | Reject missing columns, empty phase definitions, invalid phase values, invalid cell counts, and changed input sizes. | +| `ReadCtfDataFilter-D4` | Multi-slice CTF imports silently kept only the first slice. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | Use the header's Z extent and positive Z spacing; default missing spacing to 1. | +| `ReadCtfDataFilter-D5` | EDAX alignment omitted the 6/m hexagonal class. | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was affected. | Add 30 degrees to phi2 for both 6/m and 6/mmm before optional radian conversion. | +| `ReadCtfDataFilter-D6` | Float32 intermediates changed the rounded Euler-angle results. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | Use double-precision intermediates before storing each float32 result. | +| `ReadCtfDataFilter-D7` | Some reader failures produced error code zero. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | Use the negative reader return code when its error-code member is not negative. | ## Code path coverage -*20 of 23 enumerated paths exercised; the gaps are one passthrough needing permission manipulation (row 1), the file-changed phase-count guard (row 9b), and the cancel checks (row 21). Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ReadCtfData.cpp` (285 lines) + preflight in `Filters/ReadCtfDataFilter.cpp` (268 lines).* Logical phases: **(a)** preflight (header-only read → output actions), **(b)** execute read + ensemble population (`loadMaterialInfo`), **(c)** cell-data copy (`copyRawEbsdData`). - -| # | Phase | Path | Test case | -|----|-------|------|-----------| -| 1 | (a) Preflight | `readHeaderOnly` error → passthrough | *Not directly tested — `FileSystemPathParameter` rejects missing files first; an unreadable-but-present file needs permission manipulation. Same gap accepted for ReadAngDataFilter row 1.* | -| 2 | (a) Preflight | `XCells/YCells < 1` or `ZCells < 0` → `-19604` | `Invalid Cells Preflight Error (-19604)` (DYNAMIC_SECTION: zero XCells; negative ZCells) | -| 3 | (a) Preflight | Geometry action: dims (X,Y,Z≥1), spacing (XStep,YStep,ZStep or 1.0), origin (0,0,0), Micrometer | `Class 1 Analytical Oracle` (2D defaults) + `3D Multi-Slice CTF` (both ZStep variants) | -| 4 | (a) Preflight | Per-column array creation with int32/float32 type dispatch from `CtfFields` | `Class 1 Analytical Oracle` (all 9 cell arrays exist with right types) | -| 5 | (a) Preflight | Ensemble AM sized `phases + 1`; CrystalStructures/LatticeConstants/MaterialName actions | `Class 1 Analytical Oracle` (3 tuples) + `3D Multi-Slice CTF` (2 tuples) | -| 6 | (a) Preflight | Scan/phase preflight-info values (incl. Z line when ZCells > 1) | *Exercised implicitly by every preflight; display-only values, not asserted.* | -| 7 | (b) Execute | `readFile()` error → passthrough (with error-code fallback) | `Zero Step (-102)` (fallback path), `Truncated Data (-105)`, `Unknown Column (-107)` *(other reader codes are the same return statement)* | -| 8 | (b) Execute | `phases.empty()` → `-19600` | `Empty Phases rejected (-19600)` | -| 9 | (b) Execute | All-slot Invalid-Phase default initialization | `Class 1 Analytical Oracle` (slot 0). *Uncovered-slot case is unreachable for CTF — CtfReader assigns indices 1..N contiguously.* | -| 9b | (b) Execute | phase index `>= numTuples` → `-19605` | *Not directly tested. Only reachable if the file gains a phase between the execute-time preflight and the algorithm's re-read — requires file-mutation injection inside a single execute call. Mirrors ReadAngDataFilter's untested `-19504`.* | -| 10 | (b) Execute | Per-phase fill: Laue→structure index, name, lattice constants | `Class 1 Analytical Oracle` (2 phases, 2 Laue classes) | -| 11 | (c) Copy | reader element count `< totalCells` → `-19603` | `Reader/Geometry Cell Count Mismatch rejected (-19603)` — static `ZCells 0` fixture trips it deterministically | -| 12 | (c) Copy | missing data column → `-19601` | `Missing Data Column rejected (-19601)` | -| 13 | (c) Copy | phase value out of `[0, phases]` → `-19602` | `Out-of-Range Phase Value rejected (-19602)` | -| 14 | (c) Copy | Phase verbatim copy (Phase 0 preserved) | `Class 1 Analytical Oracle` (points 2, 5) | -| 15 | (c) Copy | Euler interleave `3i/3i+1/3i+2` | `Class 1 Analytical Oracle` (18 values) | -| 16 | (c) Copy | Hex-alignment +30° branch (Hexagonal_High points only, option on) | `Euler Conversion Combinations` (points 0/3 shifted; cubic and Phase-0 points pinned unshifted) | -| 17 | (c) Copy | Degrees→radians branch | `Euler Conversion Combinations` (both radians combos) | -| 18 | (c) Copy | 7 verbatim pass-through columns (Bands, Error, MAD, BC, BS, X, Y) | `Class 1 Analytical Oracle` (element-wise) | -| 19 | (c) Copy | Multi-slice data volume (x·y·z cells) | `3D Multi-Slice CTF` (8 cells across 2 slices, element-wise) | -| 20 | — | SIMPL 6.4/6.5 parameter conversion | `SIMPL Backwards Compatibility` | -| 21 | (b)/(c) | Cancel checks (3 sites) | *Not directly tested. Requires cancel-signal injection; standard early-return pattern. Excluded from scope by direction.* | -| 23 | OOC boundary | 65536-block conversion oracle | `real HDF5 65536-block conversion oracle` — independent expected outputs on actual HDF5 stores | +18 of 33 paths exercised. The count uses default test cases with asserted results. + +Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ReadCtfData.cpp` (287 lines), `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ReadCtfDataFilter.cpp` (277 lines), and `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/utilities/EbsdReaderUtilities.hpp` (332 lines). + +Test names below omit the common `OrientationAnalysis::ReadCtfDataFilter: ` prefix. + +| # | Phase | Path | Test case | +|---|-------|------|-----------| +| 1 | Preflight | Header read fails; propagate its error with a return-code fallback | *Not directly tested. No fixture fails the header read after file-path validation.* | +| 2 | Preflight | X cell count below 1 → `-19604` | `Invalid Cells Preflight Error (-19604)` / `Zero XCells` | +| 3 | Preflight | Y cell count below 1 → `-19604` | *Not directly tested. The invalid-dimension fixture sets X to zero.* | +| 4 | Preflight | Negative Z cell count → `-19604` | `Invalid Cells Preflight Error (-19604)` / `Negative ZCells` | +| 5 | Preflight | Create a 2D geometry with origin zero and micrometer units | `Class 1 Analytical Oracle` | +| 6 | Preflight | Create a multi-slice geometry with positive Z spacing | `3D Multi-Slice CTF` / `ZStep 0.75` | +| 7 | Preflight | Missing Z spacing → default to 1 | `3D Multi-Slice CTF` / `ZStep absent` | +| 8 | Preflight | Negative Z spacing → default to 1 | *Not directly tested. No fixture supplies negative ZStep.* | +| 9 | Preflight | Create int32 and float32 cell arrays | `Class 1 Analytical Oracle` — all nine cell arrays | +| 10 | Ensemble | Allocate phase count plus one; fill invalid slot and phase metadata | `Class 1 Analytical Oracle` — all three ensemble arrays | +| 11 | Preflight | Generate scan and phase display text | *Not directly tested. Tests do not assert display strings.* | +| 12 | Read | Propagate reader failures, including the fallback code | `EbsdLib Error Passthrough - Zero Step (-102)`, `EbsdLib Error Passthrough - Truncated Data (-105)`, `EbsdLib Error Passthrough - Unknown Column (-107)`, `Reader/Geometry Cell Count Mismatch rejected (-101105)` | +| 13 | Ensemble | No phase definitions → `-19600` | `Empty Phases rejected (-19600)` | +| 14 | Ensemble | Phase index exceeds preflight allocation → `-19605` | *Not directly tested. Requires a file change between preflight and the algorithm read.* | +| 15 | Copy | Reader supplies fewer cells than the geometry → `-19603` | *Not directly tested. The zero-Z fixture now fails earlier with reader error `-101105`.* | +| 16 | Copy | Missing Phase or Euler column → `-19601` | *Not directly tested. The missing-column fixture removes BS only.* | +| 17 | Copy | Missing pass-through column → `-19601` | `Missing Data Column rejected (-19601)` — checks BS in the message | +| 18 | Copy | Phase equals the ensemble tuple count → `-19602` | `Out-of-Range Phase Value rejected (-19602)` — phase 3 with two defined phases | +| 19 | Copy | Negative phase → `-19602` | *Not directly tested. The invalid-phase fixture uses a positive value.* | +| 20 | Copy | Preserve phase zero and interleave Euler components | `Class 1 Analytical Oracle`; `3D Multi-Slice CTF` | +| 21 | Convert | Degrees, no hexagonal alignment | `Euler Conversion Combinations` / `Degrees, No Hex Alignment` | +| 22 | Convert | Degrees with alignment for 6/m and 6/mmm; preserve cubic and unindexed values | `Euler Conversion Combinations` / `Degrees, Hex Alignment` | +| 23 | Convert | Radians, no hexagonal alignment | `Euler Conversion Combinations` / `Radians, No Hex Alignment` | +| 24 | Convert | Radians with alignment for 6/m and 6/mmm; preserve phase values | `Euler Conversion Combinations` / `Radians, Hex Alignment` | +| 25 | Copy | Copy four int32 and three float32 pass-through columns | `Class 1 Analytical Oracle` — checks every value | +| 26 | Copy | Process a full 65,536-point block and a tail | *Not directly tested. Default cases have fewer than 65,536 points; the hidden boundary case defines this fixture.* | +| 27 | Cancellation | Return at any of four cancellation checks | *Not directly tested. Requires cancellation injection.* | +| 28 | I/O | Phase bulk write fails | *Not directly tested. Requires an injected store write failure.* | +| 29 | I/O | Crystal-structure cache read fails | *Not directly tested. Requires an injected store read failure.* | +| 30 | I/O | Euler block write fails | *Not directly tested. Requires an injected store write failure.* | +| 31 | I/O | Int32 pass-through write fails | *Not directly tested. Requires an injected store write failure.* | +| 32 | I/O | Float32 pass-through write fails | *Not directly tested. Requires an injected store write failure.* | +| 33 | Conversion | Convert SIMPL UUID/name and arguments | `SIMPL Backwards Compatibility` | ## Test inventory +All cases are in `src/Plugins/OrientationAnalysis/test/ReadCtfDataTest.cpp`. The 12 default cases run on Linux, macOS, and Windows. All pass in CI. + | Test case | Status | Notes | |-----------|--------|-------| -| `OrientationAnalysis::ReadCtfDataFilter: Class 1 Analytical Oracle` | new-for-V&V | Class 1 + 4. Geometry (dims/spacing/origin/units), 9 cell arrays and 3 ensemble arrays asserted element-wise (~80 assertions), all expected values hand-derived, float32-exact fixture. Inline data. | -| `…: Euler Conversion Combinations` | new-for-V&V | Class 1. DYNAMIC_SECTION over the 2×2 conversion-option grid; NumPy-derived correctly-rounded literals; pins hex shift scoping (never Phase-0/cubic points). | -| `…: 3D Multi-Slice CTF` | new-for-V&V | Class 1. DYNAMIC_SECTION (ZStep 0.75 / absent). Regression pin for D4 (3D support restored). | -| `…: Empty Phases rejected (-19600)` | new-for-V&V | Value-add guard. Legacy segfaults on this fixture (demonstrated, D3). | -| `…: Missing Data Column rejected (-19601)` | new-for-V&V | Value-add guard. Legacy segfaults on this fixture (demonstrated, D3). | -| `…: Out-of-Range Phase Value rejected (-19602)` | new-for-V&V | Value-add guard. Legacy silently succeeds with heap-dependent output (demonstrated, D3). | -| `…: Reader/Geometry Cell Count Mismatch rejected (-19603)` | new-for-V&V | Value-add guard. Static `ZCells 0` fixture — no file-mutation injection needed. | -| `…: Invalid Cells Preflight Error (-19604)` | new-for-V&V | Value-add preflight rejection (`XCells 0`). | -| `…: EbsdLib Error Passthrough - Zero Step (-102)` | new-for-V&V | Exercises the error-code fallback (reader sets message only). | -| `…: EbsdLib Error Passthrough - Truncated Data (-105)` | new-for-V&V | Premature-EOF return point. | -| `…: EbsdLib Error Passthrough - Unknown Column (-107)` | new-for-V&V | Column-allocation return point. | -| `…: SIMPL Backwards Compatibility` | new-for-V&V | DYNAMIC_SECTION over the existing 6.4/6.5 conversion fixtures — the filter previously had **no** conversion test despite shipping the fixtures. | -| *(retired)* `OrientationAnalysis::ReadCtfData: Valid Execution` | retired | Exemplar comparison against `6_6_read_ctf_data.dream3d` — **legacy-generated oracle** (forbidden). Replaced by the Class 1 oracle above. | -| `real HDF5 65536-block conversion oracle` | new-for-V&V | 38 assertions; A generated 65,537-point CTF file crosses the Euler interleaving block with cubic, hexagonal, and unindexed witnesses. Existing independently rounded radians/hex-alignment literals are checked exactly. Unlike ANG, the unindexed tail phase remains zero. Euler, phase, and band arrays are HDF5-OOC. | - -OOC recertification (2026-09-21): serial CTest passed 12/12 target entries in each DREAM3DNX build. The hidden `real HDF5 65536-block conversion oracle` passed 38 assertions in the OOC binary. A generated 65,537-point CTF file crosses the Euler interleaving block with cubic, hexagonal, and unindexed witnesses. Existing independently rounded radians/hex-alignment literals are checked exactly. Unlike ANG, the unindexed tail phase remains zero. Euler, phase, and band arrays are HDF5-OOC. The new case is included in the plugin's OOC store-contract CTest group. Upstream oracle assertions and tolerances remain intact. No new legacy binary comparison is claimed. +| `OrientationAnalysis::ReadCtfDataFilter: Class 1 Analytical Oracle` | new-for-V&V | Checks 3×2×1 geometry, spacing, origin, units, all nine cell arrays, all three ensemble arrays, and tuple dimensions. | +| `OrientationAnalysis::ReadCtfDataFilter: Euler Conversion Combinations` | new-for-V&V | Checks 18 Euler values, crystal structures, and unchanged phases in `Degrees, No Hex Alignment`, `Degrees, Hex Alignment`, `Radians, No Hex Alignment`, and `Radians, Hex Alignment`. Includes 6/m and 6/mmm witnesses for D5 and rounding witnesses for D6. | +| `OrientationAnalysis::ReadCtfDataFilter: 3D Multi-Slice CTF` | new-for-V&V | Checks 2×2×2 dimensions, phases, Bands, and EulerAngles in `ZStep 0.75` and `ZStep absent`; covers D4. | +| `OrientationAnalysis::ReadCtfDataFilter: Empty Phases rejected (-19600)` | new-for-V&V | Checks the explicit error for a file with no phase definitions; covers D3. | +| `OrientationAnalysis::ReadCtfDataFilter: Missing Data Column rejected (-19601)` | new-for-V&V | Removes BS and checks the error code and column name; covers D3. | +| `OrientationAnalysis::ReadCtfDataFilter: Out-of-Range Phase Value rejected (-19602)` | new-for-V&V | Uses the first invalid phase value, 3; checks the error code and valid range `[0, 2]`; covers D3. | +| `OrientationAnalysis::ReadCtfDataFilter: Reader/Geometry Cell Count Mismatch rejected (-101105)` | new-for-V&V | Sets ZCells to zero and checks the reader's raster-grid error `-101105`. Does not reach `-19603`. | +| `OrientationAnalysis::ReadCtfDataFilter: Invalid Cells Preflight Error (-19604)` | new-for-V&V | Checks `Zero XCells` and `Negative ZCells` preflight rejections; covers D3. | +| `OrientationAnalysis::ReadCtfDataFilter: EbsdLib Error Passthrough - Zero Step (-102)` | new-for-V&V | Sets XStep to zero and checks the returned reader error; covers D7. | +| `OrientationAnalysis::ReadCtfDataFilter: EbsdLib Error Passthrough - Truncated Data (-105)` | new-for-V&V | Supplies four of six data rows and checks the premature-end error. | +| `OrientationAnalysis::ReadCtfDataFilter: EbsdLib Error Passthrough - Unknown Column (-107)` | new-for-V&V | Replaces BS with Bogus and checks the column-allocation error. | +| `OrientationAnalysis::ReadCtfDataFilter: real HDF5 65536-block conversion oracle` | new-for-V&V | Hidden case. Requires HDF5 stores; checks Euler values across a 65,536-point boundary, phase-zero preservation, Bands, and array storage formats. | +| `OrientationAnalysis::ReadCtfDataFilter: SIMPL Backwards Compatibility` | new-for-V&V | `SIMPL 6.5 (UUID)` and `SIMPL 6.4 (Filter_Name)` check filter identity, comments, and all six converted arguments. | ## Exemplar archive -- **Archive:** None. **`6_6_read_ctf_data_2.tar.gz` retired** (SHA512 was `f397fa3b…ad1707`); its `download_test_data()` entry is removed from `test/CMakeLists.txt`. -- **Provenance:** `src/Plugins/OrientationAnalysis/vv/provenance/6_6_read_ctf_data_2.md` — documents the legacy-oracle status and the inline replacement. -- All current oracle data is inline in `test/ReadCtfDataTest.cpp`; there is nothing to archive. +- **Archive:** None. `6_6_read_ctf_data_2.tar.gz` is retired; current fixtures are inline. +- **SHA512:** Not applicable. `src/Plugins/OrientationAnalysis/test/CMakeLists.txt` has no ReadCtfData archive registration. +- **Provenance:** [src/Plugins/OrientationAnalysis/vv/provenance/ReadCtfDataFilter.md](provenance/ReadCtfDataFilter.md) -## Deviations from DREAM3D 6.5.171 +## Deviations from DREAM.3D v6.5.171 -*Comparison run 2026-07-24 against the official DREAM3D 6.5.171 release on four byte-identical-input fixtures (toy ×2 conversion combos, Cugrid 550×400 production scan, 2×2×2 multi-slice toy) plus three malformed-input fixtures. All numeric outputs bit-identical outside the entries below. Full write-ups: `vv/deviations/ReadCtfDataFilter.md`; working artifacts archived to OneDrive (comparison working folder).* +See [vv/deviations/ReadCtfDataFilter.md](deviations/ReadCtfDataFilter.md) for the root cause, affected users, and recommendation. This table lists only Part 1 entries. -- `ReadCtfDataFilter-D1` — Unindexed (Phase 0) points: legacy remaps to phase 1, SIMPLNX preserves 0 (deliberate, PR #937). 116,050 points on the production fixture. Trust SIMPLNX. -- `ReadCtfDataFilter-D2` — Consequence of D1: legacy applies the hexagonal +30° φ2 shift to unindexed points; SIMPLNX never does. Exactly 0.5235988 rad on affected values. Trust SIMPLNX. -- `ReadCtfDataFilter-D3` — Malformed-input behavior: legacy segfaults (missing column; empty phases — both demonstrated, exit 139) or silently emits heap-dependent output (out-of-range phase value — demonstrated, exit 0); SIMPLNX rejects with −19600…−19604. Trust SIMPLNX. -- `ReadCtfDataFilter-D4` — *(retired)* Multi-slice `.ctf` silently truncated to slice 0 by earlier DREAM3D-NX releases; resolved, now bit-identical to legacy 3D output. Trust SIMPLNX (current). -**Fixed in DREAM3D-NX 7.4.2:** `ReadCtfDataFilter-D4`. +| Deviation | Observed difference | +|-----------|---------------------| +| `ReadCtfDataFilter-D1` | SIMPLNX preserves unindexed phase zero; DREAM.3D remaps it to phase 1. | +| `ReadCtfDataFilter-D2` | SIMPLNX does not apply phase 1's hexagonal correction to unindexed points. | +| `ReadCtfDataFilter-D3` | SIMPLNX rejects malformed input that can crash DREAM.3D or cause invalid memory access. | +| `ReadCtfDataFilter-D5` | SIMPLNX applies EDAX alignment to both 6/m and 6/mmm; DREAM.3D applies it only to 6/mmm. | diff --git a/src/Plugins/OrientationAnalysis/vv/ReadEbsdPatternFileFilter.md b/src/Plugins/OrientationAnalysis/vv/ReadEbsdPatternFileFilter.md index 605f6143cc..4df21ac484 100644 --- a/src/Plugins/OrientationAnalysis/vv/ReadEbsdPatternFileFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/ReadEbsdPatternFileFilter.md @@ -4,43 +4,44 @@ |---|---| | Plugin | OrientationAnalysis | | SIMPLNX UUID | `cd975a45-53de-4ace-ac16-d99638a44d6a` | -| DREAM3D 6.5.171 equivalent | None. This is a new filter. | +| SIMPLNX Human Name | Read EBSD Pattern File | +| DREAM.3D v6.5.171 equivalent | None (new filter) | | Verified commit | ** | -| Status | DRAFT | -| Sign-off | Pending second-engineer review. | +| Status | COMPLETE | +| Sign-off | Michael Jackson, 2026-08-27; second engineer: Jared Duffey, 2026-09-15 | ## At a glance | Aspect | Current state | |---|---| -| Algorithm Relationship | **New filter.** No DREAM3D 6.5.171 filter reads EDAX UP pattern stacks into a general DataArray. | -| Oracle (confirmed) | **Class 1 (Analytical) primary and Class 4 (Invariant) companion.** The test writes fixed little-endian headers and payload values. It checks the parsed metadata, output shapes, output types, and exact pixel values. | -| Code paths enumerated | 17 of 19 paths are exercised. The out-of-core store fallback and execution-time short-read recovery path need an OOC build or a file-mutation harness. | -| Tests today | 14 test cases cover UP versions 1 and 3, `.up1` and `.up2`, output placement, warnings, malformed files, extension mismatches, and cancellation. All pass in the full 1,753-test in-core run. | -| Exemplar archive | None. The Class 1 fixtures are small files that the test creates from independent literal bytes. The test does not use a saved output from SIMPLNX or DREAM3D. | -| Legacy comparison | Not run. The filter is new and has no DREAM3D 6.5.171 equivalent. | -| Bug flags | None. | -| V&V phase | Oracle reconciliation, the full in-core build, all 1,753 in-core tests, and the clang-format gate are complete. OOC evidence and second-engineer review are pending. | +| Algorithm Relationship | New filter. DREAM.3D v6.5.171 has no equivalent EDAX UP pattern-file reader. | +| Oracle (confirmed) | Class 1 byte fixtures and Class 4 shape checks are encoded in `test/ReadEbsdPatternFileTest.cpp`. All pass in CI. | +| Code paths enumerated | 19 of 42 exercised. Gaps include input guards, warnings, file changes, chunk boundaries, and pixel assertions for two format combinations. | +| Tests today | 14 test cases cover headers, output shapes, selected pixel values, malformed inputs, warnings, and cancellation. All pass in CI. | +| Exemplar archive | None. Tests construct synthetic files and expected values in C++. | +| Legacy comparison | Not run — DREAM.3D v6.5.171 has no equivalent filter. | +| Bug flags | None | +| V&V phase | Analytical and invariant checks are encoded and pass in CI. Second-engineer review is complete. | ## Summary -This filter reads EDAX `.up1` and `.up2` EBSD detector patterns into a SIMPLNX DataArray. Class 1 fixtures verify exact header and payload behavior. Class 4 checks verify tuple-count and shape contracts. No legacy comparison applies because this is a new filter. +`ReadEbsdPatternFileFilter` reads EDAX `.up1` and `.up2` EBSD patterns into a DataArray. Class 1 fixtures check metadata, errors, warnings, and selected endpoint pixels; Class 4 checks verify tuple and component shapes. All pass in CI. ## Algorithm Relationship -*Classification:* **New filter** +*Classification:* New filter -*Evidence:* Search of the available DREAM3D 6.5.171 source trees found no filter or utility that reads `.up1` or `.up2` pattern files. This filter has a new SIMPLNX UUID and no SIMPL conversion mapping. +*Evidence:* The DREAM.3D v6.5.171 source and corresponding SIMPL and plugin sources contain no EDAX UP reader; `ReadEbsdPatternFileFilter.hpp` declares the new UUID above. ## Oracle -*Class:* **1 (Analytical) primary and 4 (Invariant) companion** +*Class:* 1 (Analytical), with 4 (Invariant) checks. -*Applied:* The test writes each integer and floating-point header field as fixed little-endian bytes. It writes deterministic pixel literals after the header. Expected metadata, shapes, types, warnings, error branches, and output pixels are derived directly from those bytes. +*Applied:* Independent little-endian writers define header fields and pixel sequences; assertions check metadata, shapes, diagnostics, and the first and last pixels for version 1 `.up2` and version 3 `.up1`. -*Encoded:* `src/Plugins/OrientationAnalysis/test/ReadEbsdPatternFileTest.cpp` contains 14 test cases. The suite checks version 1 and 3 headers, both pixel widths, exact payload values, AttributeMatrix tuple rules, manual version 1 dimensions, extra-pattern exclusion, future-version and unknown-step warnings, malformed inputs, both extension-mismatch directions, case-insensitive extensions, and cancellation. All 14 tests pass in the full 1,753-test in-core run. +*Encoded:* `src/Plugins/OrientationAnalysis/test/ReadEbsdPatternFileTest.cpp::OrientationAnalysis::ReadEbsdPatternFileFilter: Version 1 UP2 Manual Scan Dimensions` and 13 companion cases use three valid import layouts plus invalid-input and warning variants. All pass in CI. -*Second-engineer review:* Pending. +*Second-engineer review:* Jared Duffey, 2026-09-15. ## Bugs found and fixed @@ -48,16 +49,15 @@ None. ## Code path coverage -17 of 19 paths are exercised. +19 of 42 paths exercised. Each uncovered row states the missing fixture or assertion. -Sources: +Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ReadEbsdPatternFile.cpp` (72 lines); `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ReadEbsdPatternFileFilter.cpp` (223 lines); `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/utilities/EdaxUpPatternFileReader.cpp` (351 lines); `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/utilities/EbsdPatternFileReaderFactory.cpp` (21 lines); `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/utilities/EbsdPatternFileUtilities.cpp` (90 lines). -- `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ReadEbsdPatternFile.cpp` (72 lines). -- `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/utilities/EdaxUpPatternFileReader.cpp` (354 lines). +Test names below omit their `OrientationAnalysis::ReadEbsdPatternFileFilter: ` or `OrientationAnalysis::EBSD Pattern Reader: ` prefix. The tests use square patterns and square scan grids; they do not distinguish swapped axes. | # | Phase | Path | Test case | |---:|---|---|---| -| 1 | Factory | `.up1` or `.up2` selects the EDAX reader | `Case Insensitive Extension`; all valid filter cases | +| 1 | Factory | `.up1` or `.up2` selects the EDAX reader | `Version 1 UP2 Manual Scan Dimensions`; `Version 3 Header Geometry` | | 2 | Factory | Unsupported extension returns an error | `Factory Rejects Unsupported Extension` | | 3 | Header | Version 1 derives a flat pattern count from file size | `Version 1 Flat Preflight` | | 4 | Header | Version 3 reads packed fields at unaligned offsets | `Version 3 Metadata` | @@ -67,39 +67,70 @@ Sources: | 8 | Header | Residual version 1 payload bytes return an error | `Malformed Files` — residual-byte section | | 9 | Header | Version 3 grid payload is incomplete | `Malformed Files` — truncated-grid section | | 10 | Header | Pixel width conflicts with `.up1` or `.up2` | `Probable Extension Mismatch`; `Probable Wide Payload Extension Mismatch` | -| 11 | Header | Extra patterns are present and match the trailing payload | `Version 3 Extra Patterns Are Skipped` | +| 11 | Header | Extra patterns are present and match the trailing payload | `Version 3 Extra Patterns Are Skipped` — warning and output size only | | 12 | Header | Extra-pattern count and trailing payload disagree | `Version 3 Warnings` — extra-count section | | 13 | Preflight | Existing AttributeMatrix has the required tuple count | `Attribute Matrix Tuple Contract` — matching section | | 14 | Preflight | Existing AttributeMatrix has the wrong tuple count | `Attribute Matrix Tuple Contract` — mismatch section | | 15 | Preflight | Version 1 manual rows and columns match or mismatch | `Version 1 UP2 Manual Scan Dimensions`; `Invalid Version 1 Scan Dimensions` | -| 16 | Execute | Read `uint8` and `uint16` payloads in file order | `Version 1 Flat Preflight`; `Version 1 UP2 Manual Scan Dimensions`; `Version 3 Header Geometry` | +| 16 | Execute | Read `uint8` and `uint16` payloads in file order | `Version 1 UP2 Manual Scan Dimensions`; `Version 3 Header Geometry` — first and last pixels only | | 17 | Execute | Cancellation occurs before payload transfer | `Cancellation Before Payload Read` | -| 18 | Execute | Non-contiguous or out-of-core store uses sequential `setValue` writes | *Not directly tested. No DREAM3D-NX OOC build is available in this workspace.* | +| 18 | Execute | A store other than `DataStore` uses sequential `setValue` writes | *Not directly tested. The tests do not select this store path.* | | 19 | Execute | File becomes short after preflight and before a chunk read | *Not directly tested. This needs deterministic file mutation during the execution read.* | +| 20 | Header | Zero scan steps produce absent step values and a warning | `Version 3 Warnings` — zero-step section | +| 21 | Preflight | Manual row-column multiplication overflows | `Malformed Files` — overflowing manual dimensions section | +| 22 | Preflight | Manual rows or columns are zero | *Not directly tested. No fixture supplies zero manual dimensions.* | +| 23 | Preflight | An AttributeMatrix overrides supplied manual dimensions and warns | *Not directly tested. No fixture enables manual dimensions inside an AttributeMatrix.* | +| 24 | Preflight | Version 3 overrides supplied manual dimensions and warns | *Not directly tested. No fixture supplies manual dimensions for a version 3 file.* | +| 25 | Header I/O | File-size lookup, open, or seek fails | *Not directly tested. The tests create readable files and do not inject I/O failures.* | +| 26 | Header I/O | The initial header is too short or an exact header read fails | *Not directly tested. No fixture truncates a header or changes a file during header reading.* | +| 27 | Header | Version 0 is rejected; the later unsupported-version guard is unreachable | *Not directly tested. No version 0 fixture exists; the earlier check handles 0 and 2, leaving 1 or values of at least 3.* | +| 28 | Header | Data offset is at or beyond the end of the file | *Not directly tested. The offset fixture only overlaps the header.* | +| 29 | Header | A valid nonstandard data offset produces a warning | *Not directly tested. No fixture pads the header before a valid payload.* | +| 30 | Header | Version 1 has zero complete patterns | *Not directly tested. The earlier offset and residual-byte checks reject such files.* | +| 31 | Header | A scan step is negative or not finite | *Not directly tested. Step fixtures contain only positive values or zero.* | +| 32 | Header | Version 3 has an empty grid | *Not directly tested. No fixture sets a stored row or column count to zero.* | +| 33 | Header | Undeclared trailing bytes do not match the opposite pixel width | *Not directly tested. The trailing-byte fixture only tests the opposite-width error.* | +| 34 | Size checks | Header size arithmetic overflows or metadata cannot fit the platform array size | *Not directly tested. Only manual-dimension multiplication overflow has a fixture; dimension limits prevent some header overflows.* | +| 35 | Execute | Current metadata requires a different output tuple shape | *Not directly tested. No test changes metadata after preflight or calls the algorithm with a mismatched tuple shape.* | +| 36 | Execute | Output type, tuple count, or component shape fails the reader contract | *Not directly tested. No test calls the reader with an incompatible destination.* | +| 37 | Payload I/O | Opening or seeking the payload fails | *Not directly tested. No test injects failure after metadata validation.* | +| 38 | Execute | Cancellation occurs at a payload chunk boundary | *Not directly tested. The cancellation test returns before the reader is created.* | +| 39 | Execute | Import spans multiple chunks | *Not directly tested. All synthetic payloads fit in one 4 MiB chunk.* | +| 40 | Execute | A valid version 3 `.up2` file imports `uint16` pixels | *Not directly tested. The version 3 wide-pixel fixture is used only with an incorrect `.up1` extension.* | +| 41 | Execute | A valid version 1 `.up1` file imports `uint8` pixels | *Not directly tested. The tests execute this mode but assert type and shape, not pixel values.* | +| 42 | Header | A nonzero grid flag identifies hexagonal metadata | *Not directly tested. All version 3 fixtures write a zero grid flag.* | ## Test inventory +All 14 cases are in `src/Plugins/OrientationAnalysis/test/ReadEbsdPatternFileTest.cpp`. All pass in CI. + | Test case | Status | Notes | |---|---|---| -| `Version 1 UP2 Manual Scan Dimensions` | new-for-V&V | Checks little-endian `uint16` values and `{2, 2}` manual tuple shape. | -| `Version 3 Header Geometry` | new-for-V&V | Checks file-derived tuple shape and exact `uint8` values. | -| `Version 3 Metadata` | new-for-V&V | Checks packed fields, steps, grid counts, type, and pattern size. | -| `Version 3 Extra Patterns Are Skipped` | new-for-V&V | Checks warning propagation and excludes two appended patterns. | -| `Attribute Matrix Tuple Contract` | new-for-V&V | Checks matching shape inheritance and mismatch rejection. | -| `Invalid Version 1 Scan Dimensions` | new-for-V&V | Checks row-column product validation. | -| `Malformed Files` | new-for-V&V | Checks version 2, residual bytes, offsets, dimensions, truncation, and overflow. | -| `Factory Rejects Unsupported Extension` | new-for-V&V | Checks the extension-only factory contract. | -| `Probable Extension Mismatch` | new-for-V&V | Checks `.up2` with a one-byte payload. | -| `Probable Wide Payload Extension Mismatch` | new-for-V&V | Checks `.up1` with a two-byte payload. | -| `Cancellation Before Payload Read` | new-for-V&V | Checks prompt pre-cancel return and unchanged output. | -| `Version 3 Warnings` | new-for-V&V | Checks future version, zero step, and extra-size warnings. | -| `Case Insensitive Extension` | new-for-V&V | Checks uppercase `.UP1`. | -| `Version 1 Flat Preflight` | new-for-V&V | Checks default flat shape, type, component shape, and payload import. | +| `OrientationAnalysis::ReadEbsdPatternFileFilter: Version 1 UP2 Manual Scan Dimensions` | new-for-V&V | Checks `uint16` type, `{2, 2}` tuple shape, `{12, 12}` component shape, and endpoint pixels 1000 and 1575. | +| `OrientationAnalysis::ReadEbsdPatternFileFilter: Version 3 Header Geometry` | new-for-V&V | Checks file-derived shapes, `uint8` type, and first and last pixel values. | +| `OrientationAnalysis::EBSD Pattern Reader: Version 3 Metadata` | new-for-V&V | Checks version, pixel type, dimensions, pattern count, distinct X/Y steps, and square-grid flag. | +| `OrientationAnalysis::ReadEbsdPatternFileFilter: Version 3 Extra Patterns Are Skipped` | new-for-V&V | Checks warnings, four output tuples, and 576 elements after two extra patterns; does not assert pixels. | +| `OrientationAnalysis::ReadEbsdPatternFileFilter: Attribute Matrix Tuple Contract` | new-for-V&V | Checks AttributeMatrix shape inheritance and mismatch rejection with `-78040`. | +| `OrientationAnalysis::ReadEbsdPatternFileFilter: Invalid Version 1 Scan Dimensions` | new-for-V&V | Checks rejection of a manual row-column product that differs from the pattern count (`-78043`). | +| `OrientationAnalysis::ReadEbsdPatternFileFilter: Malformed Files` | new-for-V&V | Checks version 2 rejection, residual bytes, overlapping data offset, invalid width, truncated grid, and manual-dimension overflow. | +| `OrientationAnalysis::EBSD Pattern Reader: Factory Rejects Unsupported Extension` | new-for-V&V | Checks rejection of `.ebsp` by the reader factory. | +| `OrientationAnalysis::ReadEbsdPatternFileFilter: Probable Extension Mismatch` | new-for-V&V | Checks rejection of a one-byte version 3 payload named `.up2`. | +| `OrientationAnalysis::ReadEbsdPatternFileFilter: Probable Wide Payload Extension Mismatch` | new-for-V&V | Checks rejection of a two-byte version 3 payload named `.up1`. | +| `OrientationAnalysis::ReadEbsdPatternFileFilter: Cancellation Before Payload Read` | new-for-V&V | Calls the algorithm with cancellation already set; checks success and zero endpoint pixels. | +| `OrientationAnalysis::EBSD Pattern Reader: Version 3 Warnings` | new-for-V&V | Checks future-version warning, absent zero-valued steps, and inconsistent extra-pattern count warnings. | +| `OrientationAnalysis::ReadEbsdPatternFileFilter: Case Insensitive Extension` | new-for-V&V | Checks uppercase `.UP1` acceptance and output type; does not assert pixels. | +| `OrientationAnalysis::ReadEbsdPatternFileFilter: Version 1 Flat Preflight` | new-for-V&V | Checks flat tuple shape, component shape, type, tuple count, and component count; does not assert pixels. | ## Exemplar archive -No archive is required. The test creates all Class 1 inputs from literal bytes and stores all expected values in the test source. A supplementary local ctest imported all 25 patterns from a version 1, 480 x 480 file and confirmed its first and last bytes. The same test parsed a 60 x 60 version 3 file with 1,001 columns, 854 rows, and 854,854 patterns without loading its 3.1 GB payload. The machine-specific test was removed after it passed. These files are not oracle data. +- **Archive:** None. The tests create all input files in C++. +- **SHA512:** Not applicable. `src/Plugins/OrientationAnalysis/test/CMakeLists.txt` registers no archive for these tests. +- **Provenance:** `src/Plugins/OrientationAnalysis/test/ReadEbsdPatternFileTest.cpp` contains the fixture writers and expected values; no archive sidecar is required. + +## Deviations from DREAM.3D v6.5.171 -## Deviations from DREAM3D 6.5.171 +Not applicable. DREAM.3D v6.5.171 has no equivalent filter. There are no Part 1 entries in `vv/deviations/ReadEbsdPatternFileFilter.md`. -Not applicable. DREAM3D 6.5.171 has no equivalent filter. +| Deviation | Observed difference | +|---|---| +| None | No equivalent filter exists in DREAM.3D v6.5.171. | diff --git a/src/Plugins/OrientationAnalysis/vv/ReadH5OinaDataFilter.md b/src/Plugins/OrientationAnalysis/vv/ReadH5OinaDataFilter.md index ab2096cfa4..8b546d3e96 100644 --- a/src/Plugins/OrientationAnalysis/vv/ReadH5OinaDataFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/ReadH5OinaDataFilter.md @@ -1,194 +1,170 @@ # V&V Report: ReadH5OinaDataFilter -| | | -|-----------|--------------------------| -| Plugin | OrientationAnalysis | +| | | +|--------|--------------| +| Plugin | OrientationAnalysis | | SIMPLNX UUID | `fad3d47f-f1e1-4429-bc65-5e021be62ba0` | -| DREAM.3D 6.5.171 equivalent | **None.** | +| SIMPLNX Human Name | Read Oxford Aztec Data (.h5oina) | +| DREAM.3D v6.5.171 equivalent | None (new filter) | | Verified commit | ** | -| Status | COMPLETE | -| Sign-off | *Michael Jackson * 2026-08-24 | +| Status | READY FOR REVIEW | +| Sign-off | Michael Jackson, 2026-08-24; second engineer: pending | ## At a glance -| Aspect | Current state | -|------------------------|--------------------------| -| Algorithm Relationship | **New filter, no legacy equivalent.** `ReadH5OinaDataFilter` was added in SIMPLNX and first shipped in DREAM3D-NX 7.0.0. | -| Oracle (confirmed) | **Class 1 analytical + Class 4 invariant, with Class 2 independent h5py readback.** The tests create small H5OINA inputs and compare all imported values with independently derived values. | -| Code paths enumerated | **27 of 34 paths exercised.** The uncovered paths require file-permission changes, file replacement during execution, cancel injection, or HDF5 objects that the fixture writer cannot create. | -| Tests today | 20 registered tests in the filter file plus 1 hidden HDF5 batch-tail oracle; the OOC build also registers `actual OOC output parity`. Existing conversion, stacking, malformed-input, and vendor-file tests are retained. | -| Exemplar archive | **`H5Oina_Test_Data.tar.gz` retained as input only.** The vendor `.h5oina` file is used. The generated `.dream3d` output was retired as a circular oracle. | -| Legacy comparison | **Not applicable.** DREAM.3D 6.5.171 has no H5OINA importer. The independent h5py readback replaces the legacy comparison. | -| Bug flags | **Thirteen bugs resolved:** `ReadH5OinaDataFilter-D1` through `ReadH5OinaDataFilter-D13`. The affected released versions are DREAM3D-NX 7.0.0 through 7.4.1. | -| V&V phase | **COMPLETE** | +| Aspect | Current state | +|--------|---------------| +| Algorithm Relationship | New filter. DREAM.3D v6.5.171 has no H5OINA importer. | +| Oracle (confirmed) | Classes 1 and 4 use analytical fixtures and invariants in `test/ReadH5OinaDataTest.cpp`; Class 2 uses independent vendor-file readback. All pass in CI. | +| Code paths enumerated | 29 of 46 exercised. Gaps include defensive guards, cancellation, store failures, and a hidden boundary fixture. | +| Tests today | 21 test cases: 20 ordinary cases and 1 hidden boundary case. They cover analytical values, conversion options, scan order, malformed inputs, and vendor-file readback. | +| Exemplar archive | `H5Oina_Test_Data.tar.gz` supplies the vendor input. Its generated `.dream3d` output is no longer an oracle. | +| Legacy comparison | Not run — DREAM.3D v6.5.171 has no H5OINA importer. No legacy comparison date applies. | +| Bug flags | None. | +| V&V phase | Complete. Analytical fixtures and regression checks are encoded. | ## Summary -`ReadH5OinaDataFilter` imports one or more Oxford Instruments AZtec H5OINA scans into one Image Geometry. Verification uses inline Class 1 analytical data, Class 4 invariants, and an independent Class 2 h5py readback of a vendor file. The V&V found and fixed thirteen defects in the filter and EbsdLib H5OINA reader. - -## Dependency state - -- EbsdLib version 3.1.2 -- Intended DREAM3D-NX release: 7.5.0 +`ReadH5OinaDataFilter` imports Oxford Instruments AZtec H5OINA scans into one Image Geometry. Analytical fixtures, invariants, and independent vendor-file readback verify the imported values. All pass in CI; the fixed defects are listed below. ## Algorithm Relationship -**New filter, no legacy equivalent.** +*Classification:* New filter -*Evidence:* DREAM.3D 6.5.171 contains no H5OINA importer. The filter was added to SIMPLNX by PR #700 in commit `a51dd5f3d` and has no SIMPL conversion function or legacy UUID mapping. +*Evidence:* DREAM.3D v6.5.171 has no H5OINA importer; `ReadH5OinaDataFilter.cpp` has no SIMPL conversion function, and the legacy UUID mapping has no entry for this filter. ## Oracle -*Class:* **1 (Analytical) + 4 (Invariant)**, with **2 (Independent readback)** for the vendor file. +*Class:* 1 (Analytical) and 4 (Invariant), with 2 (Independent readback) for the vendor input. -*Applied:* The tests write small H5OINA files from explicit fixture specifications. Expected geometry, cell arrays, ensemble arrays, unit conversions, stacking order, and rejection results are derived from those specifications. A separate h5py script reads the vendor file and derives its expected imported values without EbsdLib or SIMPLNX. +*Applied:* Explicit scan specifications define geometry, cell and ensemble values, conversions, and errors; raw HDF5 readback supplies the vendor-file values independently of H5OINAReader. -*Encoded:* `test/ReadH5OinaDataTest.cpp` contains 20 registered test cases and the hidden `genuine HDF5 batch-tail and slab oracle`. The archived `h5oina_oracle.py` script reproduces its Class 1 values and the vendor-file readback. +*Encoded:* `src/Plugins/OrientationAnalysis/test/ReadH5OinaDataTest.cpp::Class 1 Analytical Oracle`, `Multi-Scan Slab Placement`, and `Multi-Scan Hexagonal Alignment` use three base fixtures; `Conversion Option Combinations` adds Hexagonal-Low values, and `Real AZtec File Readback` checks one vendor input. All pass in CI. -*Second-engineer review:* Pending PR review. +*Second-engineer review:* Pending second-engineer review. ## Bugs found and fixed -This branch fixes all defects in this table. The fixes are intended for DREAM3D-NX 7.5.0. EbsdLib defects require EbsdLib 3.1.2. - -| Deviation | Defect | Affected released versions | Resolution in this branch | -|---|---|---|---| -| `ReadH5OinaDataFilter-D1` | The hexagonal alignment added 30 radians instead of 30 degrees to `phi2`. | DREAM3D-NX 7.0.0 through 7.4.1. | The filter adds 30 degrees expressed in radians with a double-precision intermediate. | -| `ReadH5OinaDataFilter-D2` | The Euler destination offset used tuples instead of elements. Later scans overwrote part of an earlier scan. | DREAM3D-NX 7.0.0 through 7.4.1. | The Euler copy multiplies the tuple offset by three components. | -| `ReadH5OinaDataFilter-D3` | Hexagonal alignment repeatedly modified the first scan and did not modify later scans. | DREAM3D-NX 7.0.0 through 7.4.1. | The alignment loop uses the current scan slab. | -| `ReadH5OinaDataFilter-D4` | Pattern import was exposed but could not succeed. | DREAM3D-NX 7.0.0 through 7.4.1. | Preflight reports that H5OINA pattern import is not supported and gives an alternative. | -| `ReadH5OinaDataFilter-D5` | The gamma lattice angle copied the beta angle. | DREAM3D-NX 7.0.0 through 7.4.1. | EbsdLib copies the third lattice angle into the gamma slot. | -| `ReadH5OinaDataFilter-D6` | H5OINA lattice angles were imported in radians while other EBSD readers use degrees. | DREAM3D-NX 7.0.0 through 7.4.1. | EbsdLib converts the lattice angles to degrees. | -| `ReadH5OinaDataFilter-D7` | A phase without a required lattice dataset caused invalid access and a process crash. | DREAM3D-NX 7.0.0 through 7.4.1. | EbsdLib validates the required phase datasets before it reads their values. | -| `ReadH5OinaDataFilter-D8` | Multi-scan phase groups could exceed the ensemble bounds or describe different phases at the same index. | DREAM3D-NX 7.0.0 through 7.4.1. | Preflight validates every phase index, phase count, and phase definition in every selected scan. | -| `ReadH5OinaDataFilter-D9` | The Stacking Order setting was accepted but ignored. | DREAM3D-NX 7.0.0 through 7.4.1. | The scan order now determines which scan is placed at Z = 0. | -| `ReadH5OinaDataFilter-D10` | H5OINA reader error messages were constructed but stored as empty strings. | DREAM3D-NX 7.0.0 through 7.4.1. | EbsdLib stores and forwards the complete error message. | -| `ReadH5OinaDataFilter-D11` | The reader did not validate both cell counts and returned an error code that differed from its stored code. | DREAM3D-NX 7.0.0 through 7.4.1. | EbsdLib validates rows and columns and returns the stored error code. | -| `ReadH5OinaDataFilter-D12` | A non-numeric phase-group name caused `std::stoi()` to throw and left an HDF5 group open. | DREAM3D-NX 7.0.0 through 7.4.1. | EbsdLib validates the complete group name before it opens the group. | -| `ReadH5OinaDataFilter-D13` | Non-positive or non-finite scan spacing could create an invalid Image Geometry. | DREAM3D-NX 7.0.0 through 7.4.1. | Preflight requires finite, positive X, Y, and Z spacing. | +All defects in this table are fixed. DREAM3D-NX v7.4.2 contains the fixes for `ReadH5OinaDataFilter-D1` through `D13` and `D15` through `D18`. The fix for `ReadH5OinaDataFilter-D14` will be in the DREAM3D-NX release after v7.4.2. + +See `vv/deviations/ReadH5OinaDataFilter.md`, Part 2, for the causes and migration guidance. + +| Deviation | Defect | Affected released versions | Resolution | +|-----------|--------|----------------------------|------------| +| `ReadH5OinaDataFilter-D1` | Hexagonal alignment added 30 radians instead of 30 degrees to phi2. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | Adds pi/6 radians with a double-precision intermediate. | +| `ReadH5OinaDataFilter-D2` | Later scans overwrote earlier Euler values. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | Multiplies the destination tuple offset by three Euler components. | +| `ReadH5OinaDataFilter-D3` | Alignment repeatedly modified the first scan and skipped later scans. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | Applies alignment once to each point in the current scan slab. | +| `ReadH5OinaDataFilter-D4` | Pattern import reported missing data when the reader did not support pattern import. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | Rejects pattern import explicitly with error `-9583` and identifies an alternative. | +| `ReadH5OinaDataFilter-D5` | The gamma lattice-angle slot contained beta. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | Copies the third lattice angle into the gamma slot. | +| `ReadH5OinaDataFilter-D6` | H5OINA lattice angles used radians while the shared phase model used degrees. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | Converts lattice angles to degrees with a double-precision intermediate. | +| `ReadH5OinaDataFilter-D7` | Missing lattice datasets caused invalid access during preflight. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | Validates required phase data before indexing it. | +| `ReadH5OinaDataFilter-D8` | Phase groups could exceed ensemble bounds or give different meanings to a shared phase index. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | Validates phase indices, counts, and definitions in every selected scan. | +| `ReadH5OinaDataFilter-D9` | The Stacking Order setting had no effect. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | Reverses the selected scan list for High To Low stacking. | +| `ReadH5OinaDataFilter-D10` | Reader failures could have empty explanations or lose the specific inner error. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | Stores the composed error text and includes the inner failure in wrapper messages. | +| `ReadH5OinaDataFilter-D11` | Invalid cell counts could become unsigned dimensions; zero columns returned inconsistent error codes. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | Validates both signed cell counts before conversion and returns the stored reader error code. | +| `ReadH5OinaDataFilter-D12` | A non-numeric phase-group name caused an exception after opening the HDF5 group. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | Validates the complete positive integer name before opening the group. | +| `ReadH5OinaDataFilter-D13` | Non-positive or non-finite spacing could create invalid geometry. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | Requires finite, positive X, Y, and Z spacing. | +| `ReadH5OinaDataFilter-D14` | EDAX alignment skipped Hexagonal-Low phases. | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was not affected. | Applies pi/6 to both Hexagonal-High and Hexagonal-Low phases. | +| `ReadH5OinaDataFilter-D15` | Different scan grids could cause incomplete copies or reads beyond a scan buffer. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | Rejects incompatible scan dimensions or step sizes with `-9585`. | +| `ReadH5OinaDataFilter-D16` | A missing later scan passed preflight and failed after earlier scans had been copied. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | Checks every selected scan name before execution and reports `-9586`. | +| `ReadH5OinaDataFilter-D17` | Dataset lengths inconsistent with the header could cause out-of-bounds reads or discarded values. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | Rejects dataset extents that differ from the required point and component counts. | +| `ReadH5OinaDataFilter-D18` | An out-of-range cell Phase value could index beyond CrystalStructures. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | Validates every cell Phase value before storing it or using it for alignment. | ## Code path coverage -27 of 34 paths are exercised. +29 of 46 paths exercised. Each uncovered row states the missing fixture or the earlier guard that prevents direct coverage. -Source: `Filters/ReadH5OinaDataFilter.cpp`, `Filters/Algorithms/ReadH5OinaData.cpp` (352 lines), and the shared `utilities/IEbsdOemReader.hpp` read and ensemble-fill path. +Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ReadH5OinaData.cpp` (360 lines), `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ReadH5OinaDataFilter.cpp` (390 lines), `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/utilities/IEbsdOemReader.hpp` (193 lines), and `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/utilities/EbsdReaderUtilities.hpp` (332 lines). + +Test names omit the common `OrientationAnalysis::ReadH5OinaDataFilter: ` prefix. | # | Phase | Path | Test case | -|---|---|---|---| +|---|-------|------|-----------| | 1 | Preflight | Reject non-positive or non-finite Z spacing (`-9580`) | `Parameter Rejections` | | 2 | Preflight | Reject an empty scan selection (`-9581`) | `Parameter Rejections` | | 3 | Preflight | Reject pattern import (`-9583`) | `Parameter Rejections` | | 4 | Preflight | Fail while listing scans (`-9582`) | *Not directly tested. Requires a present file that cannot be read.* | -| 5 | Preflight | Reject a selected scan that is not in the file (`-9586`) | `Missing Scan Name rejected` | -| 6 | Preflight | Fail while reading the first scan header (`-9582`) | `Missing Lattice Angles`; `Invalid Phase Group Name` | -| 7 | Preflight | Reject cell counts below one (`-9584`) | `Invalid Cell Counts rejected` | -| 8 | Preflight | Reject non-positive or non-finite X or Y spacing (`-9591`) | `Invalid Scan Spacing rejected` | -| 9 | Preflight | Reject a phase index outside the ensemble bounds (`-9587`) | `Phase Index Out Of Range rejected` | -| 10 | Preflight | Fail while reading a later scan header (`-9582`) | `Missing Lattice Angles` | -| 11 | Preflight | Reject a later scan with a different grid (`-9585`) | `Scan Header Mismatch rejected` | -| 12 | Preflight | Reject a later scan with a different phase count (`-9589`) | `Scan Phase Count Mismatch rejected` | -| 13 | Preflight | Reject a later scan with different phase definitions (`-9590`) | `Scan Phase Definition Mismatch rejected` | -| 14 | Preflight | Create geometry dimensions, spacing, origin, and cell matrix | Class 1 oracle; multi-scan tests | -| 15 | Preflight | Create the ensemble matrix and three ensemble arrays | Class 1 oracle | +| 5 | Preflight | Reject a selected scan that is not in the file (`-9586`) | `Missing Scan Name rejected (-9586)` | +| 6 | Preflight | Fail while reading the first scan header (`-9582`) | `EbsdLib Error Passthrough - Missing Lattice Angles (-9582)`; `Invalid Phase Group Name rejected (-9582)` | +| 7 | Preflight | Reject cell counts below one (`-9584`) | `Invalid Cell Counts rejected (-9584)` | +| 8 | Preflight | Reject non-positive or non-finite X or Y spacing (`-9591`) | `Invalid Scan Spacing rejected (-9591)` | +| 9 | Preflight | Reject a phase index outside the ensemble bounds (`-9587`) | `Phase Index Out Of Range rejected (-9587)` | +| 10 | Preflight | Fail while reading a later scan header (`-9582`) | `EbsdLib Error Passthrough - Missing Lattice Angles (-9582)` | +| 11 | Preflight | Reject a later scan with a different grid (`-9585`) | `Scan Header Mismatch rejected (-9585)` | +| 12 | Preflight | Reject a later scan with a different phase count (`-9589`) | `Scan Phase Count Mismatch rejected (-9589)` | +| 13 | Preflight | Reject a later scan with different phase definitions (`-9590`) | `Scan Phase Definition Mismatch rejected (-9590)` | +| 14 | Preflight | Create geometry dimensions, spacing, origin, and cell matrix | `Class 1 Analytical Oracle`; `Multi-Scan Slab Placement` | +| 15 | Preflight | Create the ensemble matrix and three ensemble arrays | `Class 1 Analytical Oracle` | | 16 | Preflight | Create nine cell arrays and select the Phase type | `Conversion Option Combinations` | | 17 | Preflight | Generate display-only scan and phase information | *Not directly tested. The values are shown only in preflight.* | -| 18 | Execute | Propagate an EbsdLib data-read failure (`-8970`) | `Missing Data Column` | +| 18 | Execute | Propagate an EbsdLib data-read failure (`-8970`) | `EbsdLib Error Passthrough - Missing Data Column (-8970)` | | 19 | Execute | Reject an empty phase vector (`-8971`) | *Not directly tested. H5OINAReader rejects this file during preflight first.* | -| 20 | Execute | Initialize ensemble tuple 0 and fill phase tuples | Class 1 oracle; real-file readback | +| 20 | Execute | Initialize ensemble tuple 0 and fill phase tuples | `Class 1 Analytical Oracle`; `Real AZtec File Readback` | | 21 | Execute | Apply Low-to-High or High-to-Low stacking | `Stacking Order` | | 22 | Copy | Fail when the file cannot be reopened (`-34971`) | *Not directly tested. Requires file replacement during execution.* | | 23 | Copy | Skip an extent probe that cannot inspect a dataset | *Not directly tested. The reader rejects a missing required dataset first.* | -| 24 | Copy | Reject a dataset extent that differs from the header (`-34971`) | `Dataset Extent Mismatch rejected` | -| 25 | Copy | Copy the four `uint8` arrays into the current scan slab | Class 1 oracle; multi-scan placement | +| 24 | Copy | Reject a dataset extent that differs from the header (`-34971`) | *Not directly tested. The current reader rejects the short-data fixture before this guard.* | +| 25 | Copy | Copy four uint8 arrays into the current scan slab | `Class 1 Analytical Oracle`; `Multi-Scan Slab Placement` | | 26 | Copy | Copy Euler values with a three-component offset | `Multi-Scan Slab Placement` | -| 27 | Copy | Reject a Phase value outside the ensemble bounds (`-34972`) | `Out-of-Range Phase Value rejected` | +| 27 | Copy | Reject a Phase value outside the ensemble bounds (`-34972`) | `Out-of-Range Phase Value rejected (-34972)` | | 28 | Copy | Widen Phase to `int32` or retain `uint8` | `Conversion Option Combinations` | -| 29 | Copy | Copy MAD, X, and Y into the current scan slab | Class 1 oracle; multi-scan placement | -| 30 | Copy | Apply hexagonal alignment only to Hexagonal-High points in the current slab | Class 1 oracle; option sweep; multi-scan alignment | -| 31 | Execute/Copy | Return at the three cancellation checks | *Not directly tested. Requires cancel-signal injection.* | -| 32 | EbsdLib | Reject a noncanonical phase-group name (`-90034`, surfaced as `-9582`) | `Invalid Phase Group Name rejected` | +| 29 | Copy | Copy MAD, X, and Y into the current scan slab | `Class 1 Analytical Oracle`; `Multi-Scan Slab Placement` | +| 30 | Copy | Align Hexagonal-High and Hexagonal-Low points in each slab for both Phase types | `Conversion Option Combinations`; `Multi-Scan Hexagonal Alignment` | +| 31 | Execute/Copy | Cancel before or after scan operations, during copy/alignment batches, or before alignment | *Not directly tested. Requires cancellation injection.* | +| 32 | EbsdLib | Reject a noncanonical phase-group name (`-90034`, surfaced as `-9582`) | `Invalid Phase Group Name rejected (-9582)` | | 33 | EbsdLib | Reject an unreadable phase group, lattice dimensions, or Laue group | *Not directly tested. The fixture writer cannot create the unreadable-group case; the other required-dataset cases use the same checked path as lattice angles.* | -| 34 | Bounded I/O | Full 65,536-point batch, one-point tail, and second-scan offset | `genuine HDF5 batch-tail and slab oracle` — all nine imported arrays have independent boundary values | - +| 34 | Bounded I/O | Copy full 65,536-value batches, partial tails, and a second scan | *Not directly tested. The boundary fixture is hidden from default test selection.* | +| 35 | EbsdLib | Read version 5.0, version 2.0, or no Format Version dataset | `Format Version Variants` | +| 36 | Copy | Disable EDAX alignment and preserve Euler values | `Conversion Option Combinations` | +| 37 | Copy | Leave cubic and unindexed Euler values unchanged with alignment enabled | `Conversion Option Combinations` | +| 38 | Store I/O | Propagate a raw-array batch write error | *Not directly tested. Requires an injected destination-store failure.* | +| 39 | Store I/O | Propagate a CrystalStructures read error during alignment | *Not directly tested. Requires an injected ensemble-store failure.* | +| 40 | Store I/O | Propagate a Phase batch read error during alignment | *Not directly tested. Requires an injected Phase-store failure.* | +| 41 | Store I/O | Propagate an Euler batch read error during alignment | *Not directly tested. Requires an injected Euler-store read failure.* | +| 42 | Store I/O | Propagate an Euler batch write error after alignment | *Not directly tested. Requires an injected Euler-store write failure.* | +| 43 | Store I/O | Propagate an int32 Phase batch write error | *Not directly tested. Requires an injected Phase-store write failure.* | +| 44 | Preflight | Reject invalid spacing in a later scan (`-9591`) | *Not directly tested. The spacing fixture modifies only the first scan.* | +| 45 | Preflight | Reject a later phase index absent from the first phase list (`-9587`) | *Not directly tested. Canonical unique names, equal counts, and range validation prevent this case.* | +| 46 | EbsdLib | Reject short Data datasets before the filter extent guard (`-8970`) | `Dataset Extent Mismatch rejected (-8970)` | ## Test inventory +Names below omit the common `OrientationAnalysis::ReadH5OinaDataFilter: ` prefix. The 20 ordinary cases are the CI test set. All pass in CI. + | Test case | Status | Notes | -|---|---|---| -| `Class 1 Analytical Oracle` | new-for-V&V | Verifies geometry, all cell arrays, all ensemble arrays, alignment, and invariants. | -| `Conversion Option Combinations` | new-for-V&V | Covers the 2 x 2 alignment and Phase-type option matrix. | -| `Multi-Scan Slab Placement` | new-for-V&V | Verifies all scan slabs with disjoint values. | -| `Multi-Scan Hexagonal Alignment` | new-for-V&V | Verifies alignment on each scan slab. | -| `Format Version Variants` | new-for-V&V | Covers version 5.0, version 2.0, and no version dataset. | -| `Parameter Rejections` | new-for-V&V | Covers Z spacing, empty selection, and unsupported pattern import. | -| `Invalid Cell Counts rejected` | new-for-V&V | Covers zero and negative cell counts. | -| `Scan Header Mismatch rejected` | new-for-V&V | Covers grid dimensions and step-size differences. | -| `Missing Scan Name rejected` | new-for-V&V | Covers missing first and later scan names. | -| `Phase Index Out Of Range rejected` | new-for-V&V | Covers invalid phase indices in the first and later scans. | -| `Invalid Phase Group Name rejected` | new-for-V&V | Verifies that EbsdLib reports a non-numeric group name without throwing. | -| `Scan Phase Count Mismatch rejected` | new-for-V&V | Covers later scans with more or fewer phase groups. | -| `Scan Phase Definition Mismatch rejected` | new-for-V&V | Covers material, Laue, space-group, lattice-dimension, and lattice-angle differences. | -| `Invalid Scan Spacing rejected` | new-for-V&V | Covers zero, negative, and non-finite scan spacing. | -| `Dataset Extent Mismatch rejected` | new-for-V&V | Verifies header and dataset extent agreement. | -| `Out-of-Range Phase Value rejected` | new-for-V&V | Verifies Phase values before ensemble indexing. | -| `Missing Data Column` | new-for-V&V | Verifies EbsdLib execute error propagation. | -| `Missing Lattice Angles` | new-for-V&V | Verifies EbsdLib preflight error propagation for first and later scans. | -| `Stacking Order` | new-for-V&V | Verifies both scan orders. | -| `Real AZtec File Readback` | kept, modified | Uses the vendor H5OINA input and independent readback. The generated DREAM3D exemplar is not used. | -| `InValid Filter Execution` | retired | The old test used a missing file and did not verify the intended error. | -| `genuine HDF5 batch-tail and slab oracle` | new-for-V&V | Two 65,537-point scans; all nine cell arrays use HDF5-OOC. Exact values distinguish the full-batch end, partial tail, next scan, and final tuple. | - - -The new fixture uses the existing `ScanSpec`, `WriteH5OinaFixture`, and `MakeArgs` helpers. Expected Euler values retain the independently derived float32 rounding literals. The fixture includes hexagonal, cubic, and unindexed boundary points and verifies phase widening. Upstream/develop independent-oracle assertions remain unchanged. The existing D1–D13 dispositions are retained; DREAM.3D 6.5.171 still has no corresponding importer. - -## Test sensitivity verification - -Test sensitivity verification introduces one temporary defect at a time and confirms that the applicable test fails. - -Eleven temporary defects were evaluated. Each defect caused the expected V&V test to fail. - -| Temporary defect | Test that detected the defect | Result | -|---|---|---| -| Restore the 30-radian alignment value. | Class 1 oracle; option sweep; multi-scan alignment; format variants | Detected | -| Use a float32 intermediate for alignment. | Class 1 oracle; option sweep; multi-scan alignment; format variants | Detected | -| Use the tuple offset for the Euler element offset. | Multi-scan slab placement; multi-scan alignment | Detected | -| Apply alignment from tuple 0 for every scan. | Multi-scan alignment | Detected | -| Copy beta into the gamma lattice slot. | Class 1 oracle | Detected | -| Leave lattice angles in radians. | Class 1 oracle; real-file readback | Detected | -| Remove the dataset-extent validation. | Dataset extent mismatch | Detected | -| Pass an invalid phase-group name to `std::stoi()`. | Invalid phase group name | Detected | -| Disable phase-definition validation. | Scan phase definition mismatch | Detected | -| Disable spacing validation. | Parameter rejections; invalid scan spacing | Detected | -| Remove the file path from the Phase-range error. | Out-of-range Phase value | Detected | - -OOC recertification, 2026-09-18: serial CTest passed 20/20 in `NX-Com-Qt69-Vtk96-Rel` and 21/21 in `NX-Com-Qt69-Vtk96-OoC-Rel`. The new hidden boundary case passed 166 assertions in the OOC binary and is included in the OOC-only `OrientationAnalysisOocStoreContracts` CTest entry. The original report status and sign-off above are historical and unchanged. +|-----------|--------|-------| +| `Class 1 Analytical Oracle` | new-for-V&V | Checks geometry, all nine cell arrays, three ensemble arrays, exact alignment values, and tuple invariants. | +| `Conversion Option Combinations` | new-for-V&V | Checks all four alignment/Phase-type combinations, both hexagonal classes, and unchanged cubic and unindexed points. | +| `Multi-Scan Slab Placement` | new-for-V&V | Checks all nine arrays across two cubic scans with distinct values. | +| `Multi-Scan Hexagonal Alignment` | new-for-V&V | Checks both hexagonal classes in both scan slabs with exact Euler expectations. | +| `Format Version Variants` | new-for-V&V | Checks equivalent Euler output for version 5.0, version 2.0, and an absent version dataset. | +| `Parameter Rejections` | new-for-V&V | Checks zero and NaN Z spacing, empty selection, and unsupported pattern import. | +| `Invalid Cell Counts rejected (-9584)` | new-for-V&V | Checks zero X, zero Y, and negative X cell counts. | +| `Scan Header Mismatch rejected (-9585)` | new-for-V&V | Checks different grid dimensions and X step values in the second scan. | +| `Missing Scan Name rejected (-9586)` | new-for-V&V | Checks missing first and second scan names. | +| `Phase Index Out Of Range rejected (-9587)` | new-for-V&V | Checks phase-group index 7 with one declared phase in first and later scans. | +| `Invalid Phase Group Name rejected (-9582)` | new-for-V&V | Checks the non-numeric group name phase-one and file/group error context. | +| `Scan Phase Count Mismatch rejected (-9589)` | new-for-V&V | Checks later scans with more or fewer phase groups. | +| `Scan Phase Definition Mismatch rejected (-9590)` | new-for-V&V | Checks material name, Laue group, space group, lattice dimensions, and lattice angles. | +| `Invalid Scan Spacing rejected (-9591)` | new-for-V&V | Checks zero X step, negative Y step, and NaN X step. | +| `Dataset Extent Mismatch rejected (-8970)` | new-for-V&V | Checks the reader rejection `-101100`, wrapped as `-8970`, for 8 data tuples under a 16-point header. | +| `Out-of-Range Phase Value rejected (-34972)` | new-for-V&V | Checks invalid Phase values and error context before ensemble indexing. | +| `EbsdLib Error Passthrough - Missing Data Column (-8970)` | new-for-V&V | Checks execute error propagation for a missing required Bands dataset. | +| `EbsdLib Error Passthrough - Missing Lattice Angles (-9582)` | new-for-V&V | Checks missing lattice angles in first and later scan headers without a crash. | +| `Stacking Order` | new-for-V&V | Checks Euler, Band Contrast, and Phase values for both scan orders. | +| `genuine HDF5 batch-tail and slab oracle` | new-for-V&V | Hidden boundary fixture: checks all nine cell arrays at batch ends, tails, and scan transitions in two 65,537-point scans. | +| `Real AZtec File Readback` | kept | Uses the vendor input and independent H5Lite readback of all nine cell arrays; the generated exemplar is no longer used. | ## Exemplar archive -`H5Oina_Test_Data.tar.gz` is retained because it contains a vendor H5OINA input. The generated DREAM3D output was a circular oracle and was retired for this filter. The replacement uses inline Class 1 analytical data, Class 4 invariants, and the Class 2 h5py readback. - -SHA512: `346573ac6b96983680078e8b0a401aa25bd9302dff382ca86ae4e503ded6db3947c4c5611ee603db519d8a8dc6ed35b044a7bfea9880fade5ab54479d140ea03`. - -## Deviations from DREAM.3D 6.5.171 - -DREAM.3D 6.5.171 has no H5OINA importer. Therefore, no legacy comparison is possible. +- **Archive:** `H5Oina_Test_Data.tar.gz` +- **SHA512:** `346573ac6b96983680078e8b0a401aa25bd9302dff382ca86ae4e503ded6db3947c4c5611ee603db519d8a8dc6ed35b044a7bfea9880fade5ab54479d140ea03` +- **Provenance:** `src/Plugins/OrientationAnalysis/vv/provenance/ReadH5OinaDataFilter.md` -The deviations document compares this verified version with `ReadH5OinaDataFilter` as shipped in DREAM3D-NX 7.0.0 through 7.4.1. See `vv/deviations/ReadH5OinaDataFilter.md` for the root cause, affected users, and recommendation for each defect. +## Deviations from DREAM.3D v6.5.171 -**Fixed in DREAM3D-NX 7.4.2:** `ReadH5OinaDataFilter-D1`-`D13`. +DREAM.3D v6.5.171 has no H5OINA importer. There are no Part 1 entries; all defect entries are in Part 2 of `vv/deviations/ReadH5OinaDataFilter.md`. | Deviation | Observed difference | -|---|---| -| `ReadH5OinaDataFilter-D1` | Hexagonal `phi2` alignment uses 30 degrees instead of 30 radians. | -| `ReadH5OinaDataFilter-D2` | Every scan writes its Euler values to the correct tuple slab. | -| `ReadH5OinaDataFilter-D3` | Hexagonal alignment modifies each scan exactly once. | -| `ReadH5OinaDataFilter-D4` | Unsupported pattern import reports a clear preflight error. | -| `ReadH5OinaDataFilter-D5` | The gamma lattice slot contains gamma instead of beta. | -| `ReadH5OinaDataFilter-D6` | Lattice angles use degrees instead of radians. | -| `ReadH5OinaDataFilter-D7` | Missing phase datasets produce an error instead of invalid access. | -| `ReadH5OinaDataFilter-D8` | Multi-scan inputs must have safe phase indices and identical phase definitions. | -| `ReadH5OinaDataFilter-D9` | Stacking Order controls the scan order. | -| `ReadH5OinaDataFilter-D10` | Reader error messages reach the user. | -| `ReadH5OinaDataFilter-D11` | Both cell counts are validated and the returned error code is consistent. | -| `ReadH5OinaDataFilter-D12` | Invalid phase-group names produce an error instead of an exception. | -| `ReadH5OinaDataFilter-D13` | Geometry spacing must be finite and positive. | +|-----------|---------------------| +| None | No equivalent legacy filter exists. | diff --git a/src/Plugins/OrientationAnalysis/vv/RotateEulerRefFrameFilter.md b/src/Plugins/OrientationAnalysis/vv/RotateEulerRefFrameFilter.md index 04c33c0abb..0711e9a082 100644 --- a/src/Plugins/OrientationAnalysis/vv/RotateEulerRefFrameFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/RotateEulerRefFrameFilter.md @@ -1,107 +1,106 @@ # V&V Report: RotateEulerRefFrameFilter -| | | -|-----------|--------------------------| -| Plugin | OrientationAnalysis | +| | | +|--------|--------------| +| Plugin | OrientationAnalysis | | SIMPLNX UUID | `0458edcd-3655-4465-adc8-b036d76138b5` | | SIMPLNX Human Name | Rotate Euler Reference Frame | -| DREAM3D 6.5.171 equivalent | `RotateEulerRefFrame` — `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/RotateEulerRefFrame.{h,cpp}` | +| DREAM.3D v6.5.171 equivalent | `RotateEulerRefFrame` | | Verified commit | ** | | Status | COMPLETE | -| Sign-off | Michael Jackson — 2026-07-16 | +| Sign-off | Michael Jackson, 2026-07-05; second engineer: Nathan Young, 2026-07-10 | ## At a glance -| Aspect | Current state | -|------------------------|--------------------------| -| Algorithm Relationship | **Port** of legacy `RotateEulerRefFrame::execute()`. Same per-tuple kernel `gNew = normalize_cols(eu2om(euler) * ax2om(axis, angle))` → `om2eu`; library swaps only (OrientationLib → EbsdLib, hand-rolled `MatrixMath` → Eigen) plus a double-precision degree→radian conversion and progress/cancel plumbing. | -| Oracle (confirmed) | **Class 1 (Analytical) primary** — 8 hand/script-derived fixtures (`AnalyticalFixtures::k_Fixtures`) with closed-form derivations for Z-axis (`phi1' = phi1 - w mod 2pi`), identity + X/111 axes, normalization, and zero-angle cases. **Class 4 (Invariant) companion** — output-range bounds, (n,w)/(-n,w) round-trip, 45°+45° = 90° composability. Cross-checked by an independent numpy script (Rowenhorst 2015 Eq. A.5/A.9 + first-principles frame-rotation derivation). All pass. | -| Code paths enumerated | 6 of 8 exercised; both cancellation branches remain outside this scope. | -| Tests today | 5 registered cases plus one hidden HDF5 boundary test. Paired CTest selections pass 5/5. | -| Exemplar archive | `ASCIIData.tar.gz` (pre-existing, shared archive) — provides the 480k-tuple legacy-parity input/comparison CSVs only. **Not an oracle** (legacy-DREAM3D provenance); the Class 1 oracle is inline in the test source. No new archive needed. | -| Legacy comparison | **Run** against DREAM3D 6.5.171 on 6 axis/angle cases × 12 orientations (shared CSV input). Max wrap-aware diff 7.2e-7 rad (float32 ULP level). Both implementations independently match the numpy oracle (NX 2.3e-7, legacy 8.1e-7). No deviations; two non-deviations documented. | -| Bug flags | None affecting output. Two robustness/policy items addressed: (1) zero-length rotation axis previously produced silent NaN corruption — preflight now rejects it (`-96200`) and the Algorithm class guards it as well (`-67050`); (2) the parallel kernel writes the in-place Euler array via `operator[]` from TBB workers — per the project thread-safety policy this is now gated with `requireArraysInMemory` so parallelization is only enabled for in-core stores (the codebase-sanctioned pattern). Legacy 6.5.171 retains the zero-axis NaN behavior (documented as a non-deviation — not output-correctness). | -| V&V phase | Historical status and sign-off retained. Section 4.3 adds real-HDF5 boundary verification and paired CTest evidence. | +| Aspect | Current state | +|--------|---------------| +| Algorithm Relationship | Port of `RotateEulerRefFrame` in DREAM.3D v6.5.171. SIMPLNX retains the matrix composition and uses double-precision transforms with serial buffered traversal. | +| Oracle (confirmed) | Class 1 analytical expectations and Class 4 invariants are encoded in `test/RotateEulerRefFrameTest.cpp`. All pass in CI. | +| Code paths enumerated | 9 of 15 exercised. Cancellation, direct algorithm rejection, empty input and I/O failures have no direct fixtures. | +| Tests today | 6 test cases: an archive regression, SIMPL conversion, eight analytical fixtures, axis rejection, three invariant sections and a hidden storage-boundary case. | +| Exemplar archive | `ASCIIData.tar.gz` supplies 480,000 input Euler tuples and comparison values. Independent expectations are defined in the test source. | +| Legacy comparison | Not run for the current algorithm against DREAM.3D v6.5.171. The 2026-09-17 comparison used DREAM3D-NX v7.4.1 and v7.4.2, before the buffered traversal change. | +| Bug flags | None. | +| V&V phase | COMPLETE. | ## Summary -`RotateEulerRefFrameFilter` performs a passive rotation of the sample reference frame by a user-supplied axis-angle pair (degrees), rewriting a float32 3-component Bunge ZXZ Euler-angle array in place via `gNew = normalize_cols(eu2om(euler) · ax2om(axis, w))` → `om2eu`. Verification used a **Class 1 (Analytical) oracle** — 8 fixtures with closed-form expected outputs derived from first principles (g' = g·R_active(n,w)) and Rowenhorst 2015 conversion equations, validated by an independent numpy script — plus **Class 4 invariants** (canonical output ranges, inverse-rotation round-trip, angle composability). SIMPLNX matched the oracle on every fixture with zero discrepancies; the 6.5.171 A/B comparison across 6 axis/angle cases shows ULP-level agreement and no deviations. +`RotateEulerRefFrameFilter` rotates the sample reference frame and replaces a three-component float32 Euler-angle array in place. Eight analytical fixtures and three invariant sections check the rotation, and an archive test checks 480,000 tuples. All pass in CI. ## Algorithm Relationship -*Classification:* **Port** +*Classification:* Port -*Evidence:* Near line-by-line translation of legacy `RotateEulerRefFrame::execute()` + `RotateEulerRefFrameImpl::convert()` (SIMPL UUID `{ef9420b2-8c46-55f3-8ae4-f53790639de4}` retained in the legacy-UUID map; SIMPL 6.4/6.5 conversion fixtures at `test/simpl_conversion/6_*/RotateEulerRefFrameFilter.json`). Identical control flow: normalize axis → per-tuple eu2om, multiply by ax2om rotation matrix, column-normalize, om2eu, write back in place, parallel over tuples (now gated with `requireArraysInMemory` per the project thread-safety policy — a non-functional change from legacy). - -*Port-time deltas:* - -1. **Orientation library**: OrientationLib `DOrientTransformsType::{ax2om, eu2om, om2eu}` → EbsdLib `ebsdlib::{AxisAngleDType, EulerDType, OrientationMatrixDType}`. EbsdLib is the direct descendant of OrientationLib; same Rowenhorst-convention equations (`epsijk = +1`). No output change (confirmed by A/B). -2. **Matrix math**: hand-rolled `MatrixMath::Multiply3x3with3x3D` + `Normalize3x3D` → Eigen row-major multiply + `colwise().normalized()`. Semantically identical column-wise normalization; legacy's per-entry `>1` clamp is unreachable except through rounding. No output change. -3. **Degree→radian conversion precision**: legacy computes `float rotAngle = angle * pi / 180.0` in **float** before the double-precision transforms; SIMPLNX converts in **double**. ULP-level output difference only (see non-deviation N1). -4. **Progress reporting + cancel checking**: legacy has neither; SIMPLNX adds throttled progress feedback and per-element `m_ShouldCancel` checks. UX-only. -5. **Zero-axis preflight guard (SIMPLNX addition)**: preflight error `-96200` for a zero-length rotation axis, which previously NaN-corrupted the array silently in both codebases. Behavior change only for invalid input. - -*Material PRs since baseline:* none identified for this filter beyond routine EbsdLib version bumps and the progress-messaging framework migration. +*Evidence:* `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/OrientationAnalysisLegacyUUIDMapping.hpp` maps the legacy UUID to this filter; both implementations compose orientation and rotation matrices, normalize columns and convert back to Euler angles. ## Oracle -*Class:* **1 (Analytical)** primary + **4 (Invariant)** companion. Class 3 (Paper-based) partially inherent — the eu2om/om2eu/ax2om conversions follow Rowenhorst et al 2015 (doi:10.1088/0965-0393/23/8/083501) Eq. A.5/A.9, but those conversions are EbsdLib's own V&V responsibility; this filter's verifiable claim is the composition `g' = g · R_active(n, w)`. +*Class:* 1 (Analytical), with 4 (Invariant) companions. -*Applied:* Expected outputs derived independently of all three codebases (SIMPLNX, EbsdLib, legacy). First-principles derivation: rotating the sample reference frame by +w (right-hand rule) about unit axis n gives new-frame coordinates `u_new = P(w)·u_old` (P = passive matrix), hence `g' = g·P(w)ᵀ = g·R_active(n,w)` with R_active the Rodrigues rotation matrix. Closed forms follow: Z-axis rotation ⇒ `phi1' = phi1 − w (mod 2π)`, Φ/phi2 unchanged; identity orientation ⇒ `g' = R_active(n,w)`, om2eu by hand (fixtures F1–F7 each carry the derivation as a source comment). A pure-numpy script implementing the derivation + Rowenhorst equations (numpy 2.0.2; no random seed — all fixtures deterministic) validates every closed form and generates the one non-closed-form fixture (F8). Script archived in the verification working folder and reproduced in the provenance sidecar. +*Applied:* Independent expectations use `g′ = g · R_active(n,w)`; invariants check output ranges, inverse rotation and composition of two 45° rotations. -*Encoded:* `test/RotateEulerRefFrameTest.cpp::"OrientationAnalysis::RotateEulerRefFrameFilter: Class 1 Analytical Fixtures"` — 8 fixtures (`AnalyticalFixtures::k_Fixtures`), tolerance 1e-5 rad, all pass. Invariants: `...::"OrientationAnalysis::RotateEulerRefFrameFilter: Class 4 Invariants"` — 3 sections (range bounds, (n,w)/(−n,w) round-trip, 45°+45°=90° composability) over a 6-orientation batch, all pass. +*Encoded:* `src/Plugins/OrientationAnalysis/test/RotateEulerRefFrameTest.cpp::OrientationAnalysis::RotateEulerRefFrameFilter: Class 1 Analytical Fixtures` contains eight fixtures; `Class 4 Invariants` contains three sections over six orientations. All pass in CI. -*Second-engineer review:* **Signed off by Michael Jackson (technical authority), 2026-07-16.** +*Second-engineer review:* Michael Jackson, 2026-07-16, as recorded in the oracle review. ## Bugs found and fixed -No new defect was found during this storage recertification. Existing fixes and deviation dispositions remain documented above and in the sidecar. +All defects in this table are fixed. DREAM3D-NX v7.4.2 contains both fixes. + +| Deviation | Defect | Affected released versions | Resolution | +|-----------|--------|----------------------------|------------| +| `RotateEulerRefFrameFilter-D1` | A zero rotation axis passed validation and entered a calculation with no defined rotation direction. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | Preflight rejects axis magnitudes below float32 epsilon with `-96200`; direct algorithm calls return `-67050`. | +| `RotateEulerRefFrameFilter-D2` | Parallel workers accessed the Euler store without a residency check, permitting unsafe access to nonresident stores. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | The current algorithm reads, rotates and writes bounded buffers serially. | ## Code path coverage -*6 of 8 paths exercised. The 2 gaps (paths 1 and 6) are both cancel branches whose true path is never taken without cancel-signal injection.* +9 of 15 paths exercised. Each uncovered path has a reason below. -Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/RotateEulerRefFrame.cpp` (90 lines). +Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/RotateEulerRefFrame.cpp` (99 lines) and `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/RotateEulerRefFrameFilter.cpp` (142 lines). -The algorithm is flat: (a) entry/setup in `operator()` (cancel check, axis normalization, parallel dispatch), (b) per-tuple kernel in `RotateEulerRefFrameImpl::convert`. +Test names below omit the `OrientationAnalysis::RotateEulerRefFrameFilter:` prefix, except for the archive test `OrientationAnalysis::RotateEulerRefFrame`. -| # | Stage | Path | Test case | +| # | Phase | Path | Test case | |---|-------|------|-----------| -| 1 | (a) Entry | Cancel-before-start → early return | *Not directly tested. Requires cancel-signal injection; two-line guard, same accepted gap as prior V&V reports.* | -| 2 | (a) Entry | Axis normalization (`axis.normalize()`) | `Class 1 Analytical Fixtures` — F5 (axis (2,0,0) ≡ (1,0,0)) | -| 3 | (a) Entry | Zero-length axis → preflight error `-96200` (SIMPLNX addition) | `Zero-Length Axis Fails Preflight` | -| 4 | (b) Kernel | Per-tuple rotate: eu2om → multiply → column-normalize → om2eu → write-back | All Class 1 fixtures + Class 4 sections + legacy-parity ASCIIData test | -| 5 | (b) Kernel | om2eu gimbal-degenerate branch (`|g22| ≈ 1`) | `Class 1 Analytical Fixtures` — F1, F6 (Φ = 0 outputs) | -| 6 | (b) Kernel | Mid-loop cancel check → early return | *Not directly tested — only the false branch runs; the true (cancel) path needs cancel-signal injection (see path 1).* | -| 7 | (b) Kernel | Progress messaging (`counter >= counterIncrement`, incl. `counterIncrement == 0` for ranges < 100) | Exercised implicitly by every test (1-tuple fixtures take the `counterIncrement == 0` branch; the 480k-tuple ASCIIData test takes the batched branch). | -| 8 | OOC boundary | 65536-block rotation oracle | `real HDF5 65536-block rotation oracle` — independent expected outputs on actual HDF5 stores | +| 1 | Entry | Cancel before setup | *Not directly tested. Requires cancellation injection before execution.* | +| 2 | Preflight | Reject an axis magnitude below float32 epsilon | `Zero-Length Axis Fails Preflight` checks rejection of `(0,0,0)`. | +| 3 | Entry | Reject an invalid axis in a direct algorithm call with `-67050` | *Not directly tested. Existing fixtures call the filter and reject the axis in preflight.* | +| 4 | Entry | Normalize a non-unit axis | `Class 1 Analytical Fixtures` — `F5 unnormalized axis x90`. | +| 5 | Entry | Convert degrees and form the rotation matrix | `Class 1 Analytical Fixtures` — X, Y, Z and `(1,1,1)` rotations, including zero angle. | +| 6 | Rotation | Compose matrices, normalize columns and convert a general orientation | `Class 1 Analytical Fixtures` — F2, F3 and F8; `Class 4 Invariants`. | +| 7 | Rotation | Convert an orientation at the Euler singularity | `Class 1 Analytical Fixtures` — F1 has output Phi equal to zero. | +| 8 | Traversal | Process a full 65,536-tuple buffer | `OrientationAnalysis::RotateEulerRefFrame` checks all values across seven full buffers. | +| 9 | Traversal | Process a partial final buffer | `OrientationAnalysis::RotateEulerRefFrame` checks the 21,248-tuple tail; `Class 1 Analytical Fixtures` checks one-tuple inputs. | +| 10 | Traversal | Cancel before processing a buffer | *Not directly tested. Requires cancellation injection during traversal.* | +| 11 | I/O | Return a buffer-read error | *Not directly tested. No fixture injects a store read failure.* | +| 12 | I/O | Return a buffer-write error | *Not directly tested. No fixture injects a store write failure.* | +| 13 | I/O | Write rotated values back into the same array | `Class 1 Analytical Fixtures`; `OrientationAnalysis::RotateEulerRefFrame`. | +| 14 | Progress | Update progress after a successful buffer write | `OrientationAnalysis::RotateEulerRefFrame` traverses multiple buffers; message contents are not asserted. | +| 15 | Traversal | Skip the buffer loop for zero tuples | *Not directly tested. All fixtures contain at least one tuple.* | ## Test inventory +The five ordinary cases pass on Linux, macOS and Windows. All pass in CI. + | Test case | Status | Notes | |-----------|--------|-------| -| `OrientationAnalysis::RotateEulerRefFrame` | kept — **reclassified as legacy-parity regression pin** | 480k-tuple ASCIIData run, axis (1,1,1) w=30°, compared to `EulersRotated.csv` (legacy-DREAM3D-generated — *not an oracle*, see provenance). Retained for large-array/parallel-path coverage and historical parity; 1.44M element-wise assertions at 1e-4 tolerance. | -| `OrientationAnalysis::RotateEulerRefFrameFilter: SIMPL Backwards Compatibility` | kept | DYNAMIC_SECTION over SIMPL 6.4 + 6.5 conversion fixtures; validates UUID + `FloatVec3p1FilterParameterConverter` axis+angle merge + array-path decoding. | -| `OrientationAnalysis::RotateEulerRefFrameFilter: Class 1 Analytical Fixtures` | new-for-V&V | 8 DYNAMIC_SECTIONs (F1–F8), each with derivation comment and 3 component assertions at 1e-5 rad against the independent oracle. | -| `OrientationAnalysis::RotateEulerRefFrameFilter: Zero-Length Axis Fails Preflight` | new-for-V&V | Pins the `-96200` preflight guard added during the algorithm-review pass (previously silent NaN corruption). | -| `OrientationAnalysis::RotateEulerRefFrameFilter: Class 4 Invariants` | new-for-V&V | 3 SECTIONs over a 6-orientation batch: canonical range bounds; (n,w)/(−n,w) round-trip at 1e-4; 45°+45° = 90° composability at 1e-4. | -| `real HDF5 65536-block rotation oracle` | new-for-V&V | 18 assertions; The existing identity and wraparound analytical fixtures are placed at tuples 65,535 and 65,536. A +90-degree frame rotation about Z gives [3*pi/2,0,0] and [3*pi/2+0.1,0.5,0.3], within the original 1e-5-radian absolute tolerance. The in-place Euler store is HDF5-OOC before and after execution. | - -All 5 TEST_CASEs (13 ctest sections) pass at the working commit (in-core build). OOC/dual-build run deferred: the algorithm has no OOC-specific variant (direct `Float32Array` access) and no OOC build is configured in this workspace — same disposition as `BadDataNeighborOrientationCheckFilter`. - -OOC recertification (2026-09-21): serial CTest passed 5/5 target entries in each DREAM3DNX build. The hidden `real HDF5 65536-block rotation oracle` passed 18 assertions in the OOC binary. The existing identity and wraparound analytical fixtures are placed at tuples 65,535 and 65,536. A +90-degree frame rotation about Z gives [3*pi/2,0,0] and [3*pi/2+0.1,0.5,0.3], within the original 1e-5-radian absolute tolerance. The in-place Euler store is HDF5-OOC before and after execution. The new case is included in the plugin's OOC store-contract CTest group. Upstream oracle assertions and tolerances remain intact. No new legacy binary comparison is claimed. +| `OrientationAnalysis::RotateEulerRefFrame` | kept | Imports 480,000 Euler tuples, rotates 30° about `(1,1,1)` and checks all 1,440,000 values against `EulersRotated.csv` at absolute tolerance `1e-4`. This is an archive regression. | +| `OrientationAnalysis::RotateEulerRefFrameFilter: SIMPL Backwards Compatibility` | kept | `SIMPL 6.5 (UUID)` and `SIMPL 6.4 (Filter_Name)` sections check successful loading, filter identity, empty comments and the Euler array path. | +| `OrientationAnalysis::RotateEulerRefFrameFilter: Class 1 Analytical Fixtures` | new-for-V&V | Eight dynamic sections check three components each at `1e-5` rad: identity, general orientation, wraparound, X rotation, axis normalization, zero angle, `(1,1,1)` rotation and Y rotation. | +| `OrientationAnalysis::RotateEulerRefFrameFilter: real HDF5 65536-block rotation oracle` | new-for-V&V | Hidden case. Checks six Euler values at tuples 65,535 and 65,536 against independent expectations at `1e-5` rad, and checks the store format before and after execution. | +| `OrientationAnalysis::RotateEulerRefFrameFilter: Zero-Length Axis Fails Preflight` | new-for-V&V | Requires invalid preflight for axis `(0,0,0)`; it does not assert the error code. Covers `RotateEulerRefFrameFilter-D1`. | +| `OrientationAnalysis::RotateEulerRefFrameFilter: Class 4 Invariants` | new-for-V&V | Three sections check inclusive float32 output ranges, inverse-rotation recovery and 45°+45° equivalence to 90° over six orientations; the latter two use `1e-4` tolerance. | ## Exemplar archive -- **Archive:** `ASCIIData.tar.gz` (pre-existing shared archive; declared in `src/Plugins/SimplnxCore/CMakeLists.txt`) -- **SHA512:** *(shared archive; see `src/Plugins/SimplnxCore/CMakeLists.txt` — not duplicated here because this filter neither created nor versioned it)* +- **Archive:** `ASCIIData.tar.gz` +- **SHA512:** `70388864301ca1ea7fce7b1666d3abf682eee68c7d8b7a9bf532df7aff11e7ea9de7dc2dc80e33f0e363cbad023b663bff97df4be362f0312d311e9d5bedf370` - **Provenance:** `src/Plugins/OrientationAnalysis/vv/provenance/RotateEulerRefFrameFilter.md` -No new exemplar archive was created for this V&V cycle: the Class 1 oracle is encoded entirely as inline expected values in the test source, and the Class 4 invariants need no expected output. `EulersRotated.csv` inside `ASCIIData.tar.gz` is legacy-DREAM3D output (circular-oracle provenance) and is retained **only** as a legacy-parity regression pin, explicitly not as a correctness oracle. +## Deviations from DREAM.3D v6.5.171 -## Deviations from DREAM3D 6.5.171 +See `vv/deviations/RotateEulerRefFrameFilter.md` for the root cause, affected users and recommendation. Only Part 1 entries appear here. -- **No deviations observed.** Comparison run on 6 axis/angle cases × 12 orientations (`Code_Review/RotateEulerRefFrame/euler_input.csv`); max wrap-aware difference 7.2e-7 rad. Two non-deviations documented for future-engineer awareness in `vv/deviations/RotateEulerRefFrameFilter.md`: - - **N1 (precision)** — float vs double degree→radian conversion; ULP-level only. - - **N2 (precision, representation)** — 0 vs 2π canonical representation at the exact wrap boundary (observed for input (π/2, π/4, ¾π) rotated z-90°). Same physical angle. -**SIMPLNX-side fix ships in DREAM3D-NX 7.4.2** — the deviation from legacy remains, since 6.5.171 is unchanged: `Non-deviation`. +| Deviation | Observed difference | +|-----------|---------------------| +| `RotateEulerRefFrameFilter-D1` | SIMPLNX rejects a zero rotation axis before changing the Euler array; DREAM.3D v6.5.171 accepts it. | diff --git a/src/Plugins/OrientationAnalysis/vv/WritePoleFigureFilter.md b/src/Plugins/OrientationAnalysis/vv/WritePoleFigureFilter.md index ed8d063282..52188847e1 100644 --- a/src/Plugins/OrientationAnalysis/vv/WritePoleFigureFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/WritePoleFigureFilter.md @@ -1,113 +1,155 @@ # V&V Report: WritePoleFigureFilter -| | | -|-----------|--------------------------| -| Plugin | OrientationAnalysis | -| SIMPLNX UUID | 00cbb97e-a5c2-43e6-9a35-17a0f9ce26ed | -| DREAM3D 6.5.171 equivalent | WritePoleFigure (legacy SIMPL UUID `a10bb78e-fcff-553d-97d6-830a43c85385`) | +| | | +|--------|--------------| +| Plugin | OrientationAnalysis | +| SIMPLNX UUID | `00cbb97e-a5c2-43e6-9a35-17a0f9ce26ed` | +| SIMPLNX Human Name | Generate and Write Pole Figure Images | +| DREAM.3D v6.5.171 equivalent | `WritePoleFigure` (SIMPL UUID `a10bb78e-fcff-553d-97d6-830a43c85385`) | | Verified commit | ** | | Status | COMPLETE | -| Sign-off | Michael Jackson — 2026-07-16 | +| Sign-off | Michael Jackson, 2026-06-30; second engineer: Jared Duffey, 2026-07-09 | ## At a glance -| Aspect | Current state | -|------------------------|--------------------------| -| Algorithm Relationship | **Rewrite** — same intent (generate `<001>/<011>/<111>` pole figures) but a different output medium and rendering stack: legacy writes a **PDF per phase** via libharu; SIMPLNX creates an **image geometry** (+ optional intensity arrays) and optional raster image on disk, rendered by the **EbsdLib compositor**. | -| Oracle (confirmed) | Class 5 expert-visual and Class 4 wiring invariants are retained; the new Class 1 HDF5 boundary oracle checks exact discrete pole counts and the entire <001> intensity image. | -| Code paths enumerated | 10 of 14 listed paths exercised; layout selection and three defensive validation/warning rows remain unasserted. Empty-phase skipping is not separately proven. | -| Tests today | 4 registered entries in the focused selection, plus one hidden HDF5 boundary case. Paired selections pass 4/4. | -| Exemplar archive | `Pole_Figure_Exemplars_v6.tar.gz` (inputs only — 502 hex-Ti orientations + 251/251 mask). No baked image exemplar (deliberate: avoids coupling CI to EbsdLib pixel byte-identity). | -| Legacy comparison | Three-way expert-visual (6.5.171 / patched local legacy build / SIMPLNX) on hex **and** cubic. 6.5.171 matches the patched local legacy build (byte-identical). Data (pole positions, intensity, color mapping) visually identical; 4 cosmetic/rendering deviations. | -| Bug flags | **None.** All 4 deviations are cosmetic (axis/family labels, font) or an intentional rendering improvement (discrete vector markers). No correctness defect. | -| V&V phase | Historical COMPLETE status and sign-off retained. Section 4.3 adds independent HDF5 boundary evidence and paired CTest checks. | +| Aspect | Current state | +|--------|---------------| +| Algorithm Relationship | Rewrite of `WritePoleFigure` in DREAM.3D v6.5.171. SIMPLNX uses the EbsdLib compositor and adds image geometry and count-array outputs. | +| Oracle (confirmed) | Class 4 output-change invariants are encoded in three tests in `test/WritePoleFigureTest.cpp`. All pass in CI. | +| Code paths enumerated | 14 of 59 exercised. Other layouts, additional crystal classes, invalid inputs and failure returns lack direct coverage in the cited CI tests. | +| Tests today | Five test cases: three rendering invariants, one SIMPL conversion case and one hidden analytical count fixture. The four default test cases all pass in CI. | +| Exemplar archive | `Pole_Figure_Exemplars_v6.tar.gz` supplies orientation, phase, mask and ensemble inputs. Tests derive expectations without a stored image baseline. | +| Legacy comparison | Not run for the current algorithm; the available DREAM.3D v6.5.171 comparison is dated 2026-07-08 and predates algorithm changes. | +| Bug flags | None. | +| V&V phase | COMPLETE. | ## Summary -`WritePoleFigureFilter` generates `<001>/<011>/<111>` (or the hexagonal/trigonal equivalents) pole figures for each phase from per-cell Euler angles, phases, and crystal structures, optionally masked. It was verified by expert (Class 5) side-by-side comparison of hex and cubic pole figures rendered through DREAM3D 6.5.171, a patched local legacy build, and SIMPLNX (EbsdLib 3.1.0), backed by Class 4 invariant unit tests for the simplnx-unique wiring. The pole-figure data is visually identical across all versions; the only differences are cosmetic labeling/font and an intentional discrete-marker rendering improvement — four documented, non-defect deviations. +`WritePoleFigureFilter` generates pole figures from Euler angles, phase IDs and crystal structures, with an optional mask. Class 4 tests check the effect of masking, the hexagonal basis convention and discrete marker size. All pass in CI. ## Algorithm Relationship -**Rewrite** +*Classification:* Rewrite -*Evidence:* SIMPLNX inherits the legacy SIMPL UUID `a10bb78e-…`, but the implementation and output are substantially different: - -1. **Output medium** — legacy writes one **PDF per phase** to disk (libharu/HPDF); nothing enters the data structure. SIMPLNX creates an **ImageGeometry** with an RGB `Phase_N` array (and optional Float64 intensity arrays) in the DataStructure, and optionally writes a raster image (TIFF/PNG) to disk. -2. **Rendering stack** — legacy draws pole-figure chrome (circle, axes, labels, color bar) directly with libharu primitives; SIMPLNX delegates to the **EbsdLib pole-figure compositor** (`GeneratePoleFigureComposite`), whose rasterized output is byte-tested upstream in `PoleFigureCompositorTest::All_Laue_Classes`. -3. **Discrete mode** — SIMPLNX renders discrete figures with EbsdLib 3.1.0's **vector-marker renderer** (configurable **Discrete Marker Radius**), replacing legacy's single-black-pixel-per-orientation. -4. **New capabilities** — optional intensity Float64 arrays, MRD normalization, in-DataStructure image geometry, and the X‖a / X‖a* Hex/Trig basis convention parameter. - -The shared pole-figure projection math (modified Lambert for Color, stereographic for Discrete) descends from the same lineage, which is why the rendered figures agree visually. Being a Rewrite under the same UUID, the Deviations file defends the equivalence claim. +*Evidence:* `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/OrientationAnalysisLegacyUUIDMapping.hpp` maps the legacy UUID to this filter; the PDF renderer is replaced by an EbsdLib compositor with PNG, RGB image geometry and optional count-array outputs. ## Oracle -*Class:* **5 (Expert-visual)** primary, **4 (Invariant)** companion. +*Class:* 4 (Invariant); Class 5 visual comparison artifacts and a Class 1 analytical fixture are described in provenance. -*Justification for Class 5:* the filter's output is a rasterized pole-figure image whose correctness is inherently visual; legacy DREAM3D emits **only PDFs** (no numeric ground truth to diff), and the pixel-level rendering is owned and byte-tested by EbsdLib upstream. No Class 1–3 oracle fully specifies the rendered image. The analytically-checkable part (pole positions) and the simplnx-unique wiring are covered by the Class 4 invariants below. +*Applied:* Paired runs must change more than 1% of RGB components when the mask, hexagonal basis, rendering mode or marker radius changes; the basis test also checks intensity differences. -*Applied:* the same 502 hex-Ti orientations (and a cubic-relabeled variant) were rendered through DREAM3D 6.5.171, a patched local legacy build, and SIMPLNX (EbsdLib 3.1.0) in Color and Discrete modes; a domain expert reviewed the figures side by side. Pole positions, intensity distribution, and color-intensity mapping are visually identical across all three; the only differences are the four cosmetic/rendering items in the Deviations file. +*Encoded:* `src/Plugins/OrientationAnalysis/test/WritePoleFigureTest.cpp` — `Mask filter changes the rendered pole figure`, `HexConvention choice reaches algorithm`, and `Discrete mode and marker radius reach algorithm`: three paired-output fixtures. All pass in CI. -*Encoded:* -- **Class 4 (Invariant):** `test/WritePoleFigureTest.cpp::"OrientationAnalysis::WritePoleFigureFilter: Mask filter changes the rendered pole figure"` (masked output differs from unmasked by >1% of bytes → mask is wired) and `::"…: HexConvention choice reaches algorithm"` (X‖a vs X‖a* rotates the basal families 30° in both the intensity array and the composite RGB → both plumbing paths honor the convention). Both pass against EbsdLib 3.1.0. -- **Class 5 (Expert-visual):** the hex + cubic renders (legacy PDFs + SIMPLNX PNGs), signed off. Generator scripts + pipelines are committed under `Code_Review/vv/WritePoleFigure/`; the binary renders are archived to OneDrive — see the provenance sidecar and that folder's `README.md`. -- **Class 2 (Reference), cited not duplicated:** EbsdLib `PoleFigureCompositorTest::All_Laue_Classes` pins per-Laue-class pixel reproduction. - -*Second-engineer review:* **Signed off by Michael Jackson (technical authority), 2026-07-16.** +*Second-engineer review:* Michael Jackson, 2026-07-16, as recorded for the oracle review. ## Bugs found and fixed -No new defect was found during this OOC recertification. Existing fixes and deviation dispositions remain documented in this report and its sidecar. +All defects in this table are fixed. DREAM3D-NX v7.4.2 contains these fixes. + +| Deviation | Defect | Affected released versions | Resolution | +|-----------|--------|----------------------------|------------| +| `WritePoleFigureFilter-D6` | Preflight accepted non-positive image sizes; the new marker-radius setting also lacked a lower bound during development. | DREAM3D-NX v7.0.0 through v7.4.1 lacked the image-size check. DREAM.3D v6.5.171 was affected by that omission. No release contained the marker-radius omission. | Reject image sizes below 1 with `-680002` and discrete marker radii below 1 with `-680003`. | +| `WritePoleFigureFilter-D7` | SIMPL conversion reused an input container name for the new output geometry and could fail preflight. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | Keep the default `PoleFigure` output geometry path. | +| `WritePoleFigureFilter-D8` | PNG output had a TIFF example extension and TIFF error text during development. | No released version. DREAM.3D v6.5.171 was not affected. | The example extension and writer error text identify PNG. | +| `WritePoleFigureFilter-D9` | A temporary default selected Discrete instead of Color Intensity. | No released version. DREAM.3D v6.5.171 was not affected. | Default to Color Intensity. | ## Code path coverage -10 of 14 listed simplnx-wrapper paths exercised. The filter is a wrapper around the EbsdLib compositor; per-Laue-class projection and pixel rendering are owned/tested by EbsdLib. Logical phases: (a) preflight validation + array creation, (b) parameter→enum translation, (c) per-phase mask filtering, (d) intensity generation, (e) composite image generation. - -Source: `Algorithms/WritePoleFigure.cpp` (535 lines), plus `WritePoleFigureFilter.cpp` preflight. - -| # | Phase | Path | Test case | -|----|--------------------|--------------------------------------------------------------------------|------------------------------------------------------------------| -| 1 | (b) Param→enum | GenerationAlgorithm = Color (0) | Mask + HexConvention tests; legacy A/B (hex + cubic) | -| 2 | (b) Param→enum | GenerationAlgorithm = Discrete (1) → vector markers | `Discrete mode and marker radius reach algorithm` (CI) + legacy A/B Discrete renders (hex + cubic), expert sign-off | -| 3 | (b) Param→enum | HexConvention X‖a (0) vs X‖a* (1) | `HexConvention choice reaches algorithm` (intensity + composite) | -| 4 | (b) Param→enum | ImageLayout (Horizontal/Vertical/Square) | *Not directly asserted; Horizontal exercised in all runs. EbsdLib layout enum — low-value to sweep here.* | -| 5 | (c) Mask filter | UseMask off / on | `Mask filter changes the rendered pole figure` | -| 6 | (c) Per-phase | per-phase Euler extraction; empty phase → skip | Single-phase fixtures exercise extraction. *Empty-phase skip not directly tested — defensive guard.* | -| 7 | (d) Intensity | SaveIntensityDataArrays on + NormalizeToMRD | `HexConvention choice reaches algorithm` (SaveIntensity=true, MRD=true) | -| 8 | (d) Intensity | crystal-structure dispatch (Cubic_High, Hexagonal_High) | Hex: tests + A/B; Cubic: A/B. Other Laue classes owned by EbsdLib upstream. | -| 9 | (d) Intensity | unknown crystal structure → warning, skip | *Not directly tested — defensive warning branch.* | -| 10 | (e) Composite | SaveAsImageGeometry / WriteImageToDisk; DiscreteMarkerRadius | `Discrete mode and marker radius reach algorithm` (1 px vs 10 px composites differ); A/B renders (write-to-disk); Mask/HexConv tests (image geometry) | -| 11 | (a) Preflight | mask array wrong type → error `-53900` | *Not directly tested — low-value validation branch.* | -| 12 | (a) Preflight | ImageSize ≤ 0 → error `-680002`; Discrete mode with marker radius < 1 → error `-680003` | *Guards added during review; not directly tested — low-value validation branches.* | -| 13 | (h) SIMPL convert | 6.4 / 6.5 SIMPL JSON → Arguments (legacy ImageFormat dropped, D5) | `SIMPL Backwards Compatibility` | -| 14 | OOC storage | 65536-block discrete-count oracle | `real HDF5 65536-block discrete-count oracle` — independent exact values across the boundary | +14 of 59 paths exercised. Counts include only paths mapped to the four default test cases. Each remaining row states its coverage gap. + +Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/WritePoleFigure.cpp` (535 lines) and `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/WritePoleFigureFilter.cpp` (398 lines). + +Test names omit the common `OrientationAnalysis::WritePoleFigureFilter: ` prefix. The algorithm has no cancellation check. + +| # | Phase | Path | Test case | +|---|-------|------|-----------| +| 1 | Render | Color Intensity mode | `Mask filter changes the rendered pole figure` | +| 2 | Render | Discrete mode; marker radii 1 and 10 pixels | `Discrete mode and marker radius reach algorithm` | +| 3 | Basis | X parallel to a | `HexConvention choice reaches algorithm` | +| 4 | Basis | X parallel to a* | `HexConvention choice reaches algorithm` | +| 5 | Layout | Horizontal composite | `Mask filter changes the rendered pole figure` | +| 6 | Layout | Vertical composite | *Not directly tested. No fixture selects Vertical.* | +| 7 | Layout | Square composite | *Not directly tested. No fixture selects Square.* | +| 8 | Selection | Mask disabled | `Mask filter changes the rendered pole figure` | +| 9 | Selection | Mask enabled | `Mask filter changes the rendered pole figure` | +| 10 | Selection | Count and extract phase-one Euler angles | `Mask filter changes the rendered pole figure` | +| 11 | Selection | No selected Euler angles in a phase; skip | *Not directly tested. No default fixture masks all orientations or has an empty phase.* | +| 12 | Selection | No phase after background; empty outer loop | *Not directly tested. No fixture supplies only the background ensemble.* | +| 13 | Selection | Extraction crosses the 65,536-tuple boundary | *Not directly tested. The hidden count fixture encodes this path; it is outside the cited CI selection.* | +| 14 | Intensity | Count-array output disabled | `Mask filter changes the rendered pole figure` | +| 15 | Intensity | Color count arrays with MRD normalization | `HexConvention choice reaches algorithm` | +| 16 | Intensity | Color count arrays without MRD normalization | *Not directly tested. No Color fixture disables normalization.* | +| 17 | Intensity | Discrete counts and southern-hemisphere reflection | *Not directly tested. The hidden count fixture encodes exact counts; it is outside the cited CI selection.* | +| 18 | Dispatch | Hexagonal_High intensity and RGB output | `HexConvention choice reaches algorithm` | +| 19 | Dispatch | Cubic_High intensity and RGB output | *Not directly tested. The numerical fixture is hidden; the visual comparison predates algorithm changes.* | +| 20 | Dispatch | Cubic_Low intensity and composite dispatch | *Not directly tested. No current fixture selects this crystal class.* | +| 21 | Dispatch | Hexagonal_Low intensity and composite dispatch | *Not directly tested. No current fixture selects this crystal class.* | +| 22 | Dispatch | Trigonal_High intensity and composite dispatch | *Not directly tested. No current fixture selects this crystal class.* | +| 23 | Dispatch | Trigonal_Low intensity and composite dispatch | *Not directly tested. No current fixture selects this crystal class.* | +| 24 | Dispatch | Tetragonal_High intensity and composite dispatch | *Not directly tested. No current fixture selects this crystal class.* | +| 25 | Dispatch | Tetragonal_Low intensity and composite dispatch | *Not directly tested. No current fixture selects this crystal class.* | +| 26 | Dispatch | OrthoRhombic intensity and composite dispatch | *Not directly tested. No current fixture selects this crystal class.* | +| 27 | Dispatch | Monoclinic intensity and composite dispatch | *Not directly tested. No current fixture selects this crystal class.* | +| 28 | Dispatch | Triclinic intensity and composite dispatch | *Not directly tested. No current fixture selects this crystal class.* | +| 29 | Dispatch | Unknown crystal structure; warn and omit intensity | *Not directly tested. Requires a selected phase with an unsupported crystal-structure value.* | +| 30 | Intensity | Create arrays for phase numbers above 1 | *Not directly tested. The fixtures have one material phase.* | +| 31 | Intensity | Resize and populate phase metadata | *Not directly tested. Tests do not assert metadata values.* | +| 32 | Output | Save RGB image geometry; resize and pack RGBA to RGB | `Discrete mode and marker radius reach algorithm` | +| 33 | Output | Discard temporary image geometry | *Not directly tested. Only the hidden count fixture disables image geometry output.* | +| 34 | Output | Disk output disabled | `Mask filter changes the rendered pole figure` | +| 35 | Output | Disk output enabled; existing or newly created directory | *Not directly tested. Default fixtures disable disk output.* | +| 36 | Output | Both composite outputs disabled | *Not directly tested. Only the hidden count fixture requests counts alone.* | +| 37 | Output | Compositor returns no image; skip | *Not directly tested. Requires a compositor failure fixture.* | +| 38 | Validation | Image size below 1; error -680002 | *Not directly tested. No invalid-size fixture exists.* | +| 39 | Validation | Discrete marker radius below 1; error -680003 | *Not directly tested. No invalid-radius fixture exists.* | +| 40 | Validation | Material name input is not a StringArray; error -680000 | *Not directly tested. No wrong-type fixture exists.* | +| 41 | Validation | Mask instantiation fails; error -53900 | *Not directly tested. Requires a direct algorithm call that bypasses parameter validation.* | +| 42 | Validation | Invalid hexagonal convention; error -680010 | *Not directly tested. Requires a value outside the two parameter choices.* | +| 43 | Failure | Initial image AttributeMatrix resize fails | *Not directly tested. Requires an injected resize failure.* | +| 44 | Failure | Phase read fails during the count scan | *Not directly tested. Requires an injected store read failure.* | +| 45 | Failure | Phase read fails during the extraction scan | *Not directly tested. Requires an injected store read failure.* | +| 46 | Failure | Euler read fails during the extraction scan | *Not directly tested. Requires an injected store read failure.* | +| 47 | Failure | Creation of later-phase intensity array 1 fails | *Not directly tested. Requires a second material phase and an injected creation failure.* | +| 48 | Failure | Creation of later-phase intensity array 2 fails | *Not directly tested. Requires a second material phase and an injected creation failure.* | +| 49 | Failure | Creation of later-phase intensity array 3 fails | *Not directly tested. Requires a second material phase and an injected creation failure.* | +| 50 | Failure | Transfer of intensity array 1 fails | *Not directly tested. Requires an injected store write failure.* | +| 51 | Failure | Transfer of intensity array 2 fails | *Not directly tested. Requires an injected store write failure.* | +| 52 | Failure | Transfer of intensity array 3 fails | *Not directly tested. Requires an injected store write failure.* | +| 53 | Failure | Metadata resize fails | *Not directly tested. Requires an injected StringArray resize failure.* | +| 54 | Failure | Composite AttributeMatrix resize fails | *Not directly tested. Requires an injected allocation or creation failure.* | +| 55 | Failure | RGB array creation fails | *Not directly tested. Requires an injected allocation or creation failure.* | +| 56 | Failure | RGB transfer fails | *Not directly tested. Requires an injected store write failure.* | +| 57 | Failure | Output directory creation fails; error -67020 | *Not directly tested. Requires a filesystem failure fixture.* | +| 58 | Failure | PNG writing fails; error -53900 | *Not directly tested. Requires a filesystem or writer failure fixture.* | +| 59 | Conversion | SIMPL JSON conversion; preserve default output geometry | `SIMPL Backwards Compatibility` | ## Test inventory +All five `TEST_CASE` definitions are in `src/Plugins/OrientationAnalysis/test/WritePoleFigureTest.cpp`. The four default test cases run in CI. All pass in CI. + | Test case | Status | Notes | |-----------|--------|-------| -| `Mask filter changes the rendered pole figure` | kept | Class 4 invariant: masked vs unmasked composite RGB differ by >1% of bytes. Consumes `Pole_Figure_Exemplars_v6`. Passes on EbsdLib 3.1.0. | -| `HexConvention choice reaches algorithm` | kept | Class 4 invariant: X‖a vs X‖a* differ in both the intensity array and the composite RGB (both plumbing paths). Passes on EbsdLib 3.1.0. | -| `Discrete mode and marker radius reach algorithm` | new-for-V&V | Class 4 invariant: Discrete vs Color composites differ; 1 px vs 10 px marker radii differ. Pins the GenerationAlgorithm and DiscreteMarkerRadius plumbing in CI (previously covered only by manual A/B renders). | -| `SIMPL Backwards Compatibility` | kept | 6.4 + 6.5 SIMPL JSON → Arguments round-trip. The legacy `ImageFormat` key is intentionally not converted (D5). | -| `real HDF5 65536-block discrete-count oracle` | new-for-V&V | 29 assertions; Two selected cubic orientations straddle the 65,536-tuple extraction boundary: identity and a 45-degree Z rotation. A third phase-one orientation is masked out. Independent cubic family multiplicities give total discrete counts [12,24,16]. The complete 32×32 <001> count image is compared with a constructed histogram: four center counts, four cardinal equatorial poles, and four diagonal poles after the documented row flip. This Class 1 numerical oracle supplements the historical expert-visual renderer verification. | - -OOC recertification (2026-09-21): 4/4 entries pass in both DREAM3DNX CTest selections. The hidden `real HDF5 65536-block discrete-count oracle` passes 29 assertions. Two selected cubic orientations straddle the 65,536-tuple extraction boundary: identity and a 45-degree Z rotation. A third phase-one orientation is masked out. Independent cubic family multiplicities give total discrete counts [12,24,16]. The complete 32×32 <001> count image is compared with a constructed histogram: four center counts, four cardinal equatorial poles, and four diagonal poles after the documented row flip. This Class 1 numerical oracle supplements the historical expert-visual renderer verification. Upstream oracle expectations are retained. This campaign changes no production algorithms or historical sign-offs and claims no new legacy binary comparison. +| `OrientationAnalysis::WritePoleFigureFilter: Mask filter changes the rendered pole figure` | kept | Compares masked and unmasked RGB arrays. Requires equal nonzero sizes and more than 1% differing components. | +| `OrientationAnalysis::WritePoleFigureFilter: HexConvention choice reaches algorithm` | kept | Compares both basis choices. Requires more than 1% differing basal-intensity pixels and RGB components; does not measure an exact rotation angle. | +| `OrientationAnalysis::WritePoleFigureFilter: Discrete mode and marker radius reach algorithm` | new-for-V&V | Compares Color with Discrete, and discrete radii of 1 and 10 pixels. Requires more than 1% differing RGB components in each comparison. | +| `OrientationAnalysis::WritePoleFigureFilter: real HDF5 65536-block discrete-count oracle` | new-for-V&V | Hidden fixture. Constructs two selected cubic orientations across the 65,536-tuple boundary and masks a third. Asserts count sums [12, 24, 16] and the full 32×32 <001> histogram. | +| `OrientationAnalysis::WritePoleFigureFilter: SIMPL Backwards Compatibility` | kept | Variants `SIMPL 6.5 (UUID)` and `SIMPL 6.4 (Filter_Name)` check identity and converted arguments. The output-geometry assertion checks `PoleFigure` (D7); the unused ImageFormat assertion was removed (D5). | ## Exemplar archive -- **Archive:** `Pole_Figure_Exemplars_v6.tar.gz` (inputs only — 502 hex-Ti orientations + 251/251 mask + crystal structures + phase names). -- **SHA512:** *(see `test/CMakeLists.txt` `download_test_data(... Pole_Figure_Exemplars_v6.tar.gz ...)`)* +- **Archive:** `Pole_Figure_Exemplars_v6.tar.gz` +- **SHA512:** `23ad8853c38c66f498c7a9c43d6152419506b2bcb6c7e4854728dd3ad81efd309541981bf703ad9b0a1e9b506a0098da918599d5209cb06353281d9cecf39223` - **Provenance:** `src/Plugins/OrientationAnalysis/vv/provenance/WritePoleFigureFilter.md` -- No baked image exemplar: pixel-level reproduction is owned by EbsdLib upstream; duplicating it here couples simplnx CI to EbsdLib byte-identity (the source of prior v5 baseline drift). - -## Deviations from DREAM3D 6.5.171 -Established by expert (Class 5) visual comparison on hex and cubic; 6.5.171 matches the patched local legacy build. Full renders archived to OneDrive; regeneration scripts committed under `Code_Review/vv/WritePoleFigure/`. All four are cosmetic / labeling / intentional-rendering; none is a defect. +## Deviations from DREAM.3D v6.5.171 -- `WritePoleFigureFilter-D1` — axis labels `X`/`Y` (legacy) → `A1`/`A2` (SIMPLNX). Cosmetic. -- `WritePoleFigureFilter-D2` — font/text-metrics differ (libharu Helvetica → EbsdLib canvas_ity). Library, cosmetic. -- `WritePoleFigureFilter-D3` — hex/trig per-figure family labels differ but name symmetry-equivalent families. Cosmetic. -- `WritePoleFigureFilter-D4` — Discrete markers: filled circles (configurable radius) → intentional rendering improvement over legacy single black pixels. -- `WritePoleFigureFilter-D5` — legacy Image Format choice (tif/bmp/png/pdf) dropped; SIMPLNX always writes PNG. The legacy `ImageFormat` key is not converted from SIMPL JSON. +See `vv/deviations/WritePoleFigureFilter.md` for the root cause, affected users and recommendation. This table lists Part 1 only. -See `vv/deviations/WritePoleFigureFilter.md`. +| Deviation | Observed difference | +|-----------|---------------------| +| `WritePoleFigureFilter-D1` | Sample-axis labels change from X/Y to A1/A2. | +| `WritePoleFigureFilter-D2` | Font shapes and text spacing differ. | +| `WritePoleFigureFilter-D3` | Hexagonal and trigonal family-label conventions differ. | +| `WritePoleFigureFilter-D4` | Discrete plots use filled-circle markers with a configurable radius. | +| `WritePoleFigureFilter-D5` | Disk output changes from PDF to PNG; SIMPLNX also offers image geometry and count arrays. | diff --git a/src/Plugins/OrientationAnalysis/vv/deviations/BadDataNeighborOrientationCheckFilter.md b/src/Plugins/OrientationAnalysis/vv/deviations/BadDataNeighborOrientationCheckFilter.md index b14f7fe2bb..fe63688a2c 100644 --- a/src/Plugins/OrientationAnalysis/vv/deviations/BadDataNeighborOrientationCheckFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/deviations/BadDataNeighborOrientationCheckFilter.md @@ -1,24 +1,14 @@ -# Deviations from DREAM3D 6.5.171: BadDataNeighborOrientationCheckFilter +# Deviations from DREAM.3D v6.5.171: BadDataNeighborOrientationCheckFilter -This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent (`BadDataNeighborOrientationCheck`, source at `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/BadDataNeighborOrientationCheck.{h,cpp}` in DREAM3D 6.5.171). +This file has two parts. Part 1 lists the documented differences from DREAM.3D v6.5.171 (D1 and D2). Part 2 lists the precision and input-safety defects fixed in SIMPLNX (D3 through D5). These defects also occur in DREAM.3D v6.5.171. -Entries are referenced by stable ID (`BadDataNeighborOrientationCheckFilter-D`) from the V&V report and from public migration guidance. The ID is stable across renames; the Filter UUID field is the permanent cross-reference anchor. - -## Comparison summary - -A direct A/B comparison was run on 2026-05-29 across all 27 algorithmic test fixtures defined by the engineer in the V&V test data archive (`bad_data_neighbor_orientation_check_v2/test_design.md`). Inputs were identical (same `Quats`, `Phases`, `Mask`, `CrystalStructures`, `MisorientationTolerance`, `NumberOfNeighbors`) for both implementations. - -| | Cases | Mask bytes affected | -|---|---|---| -| Bit-identical SIMPLNX = 6.5.171 | 12 of 27 (all Case 1.X.{2,3} — "should not flip" scenarios) | 0 | -| SIMPLNX ≠ 6.5.171, direction 1→0 (SIMPLNX flips, 6.5.171 misses) | 15 of 27 (all Case 1.X.1 + all Case 2.X + both Case 3.X + Case 4) | 288 | -| SIMPLNX ≠ 6.5.171, direction 0→1 (SIMPLNX correct, 6.5.171 false-flips) | 0 | 0 | - -**100% of observed diffs are direction 1→0**, consistent with a single root cause: the legacy convergence-loop bound that terminates one level early (D1 below). The D2 stale-`w` defect is real and code-evident but does not produce a 0→1 diff in any of the engineer's tests because D1 prevents the bumped count from ever crossing threshold — the two legacy bugs mask each other. +Entries use stable IDs (`BadDataNeighborOrientationCheckFilter-D`) from one sequence. The Filter UUID is the permanent cross-reference anchor. Release-source checks and comparison evidence are recorded in `../provenance/BadDataNeighborOrientationCheckFilter.md`. --- -## BadDataNeighborOrientationCheckFilter-D1 +## Part 1 — Differences from DREAM.3D v6.5.171 + +### BadDataNeighborOrientationCheckFilter-D1 | Field | Value | |---|---| @@ -26,90 +16,100 @@ A direct A/B comparison was run on 2026-05-29 across all 27 algorithmic test fix | **Filter UUID** | `3f342977-aea1-49e1-a9c2-f73760eba0d3` | | **Status** | active | -**Symptom:** DREAM3D 6.5.171 fails to flip a bad voxel whose good-neighbor count is exactly equal to the user-supplied `NumberOfNeighbors`. SIMPLNX correctly flips such voxels. Observable in 15 of the 27 V&V fixtures (288 mask bytes total) — every case where the algorithm depends on reaching the bottom level of the iterative-decay loop. - -**Root cause:** Bug in DREAM3D 6.5.171. +**Symptom:** A bad voxel can stay bad when its matching good-neighbor count equals the requested minimum. -The legacy iterative-decay loop is `while(currentLevel > m_NumberOfNeighbors)` (`Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/BadDataNeighborOrientationCheck.cpp:299`). With user-supplied `NumberOfNeighbors = N`, this walks `currentLevel` from 6 down through `N + 1` and never executes the `currentLevel == N` iteration. A bad voxel with exactly N good neighbors can therefore never be flipped — contradicting the parameter description ("Required Number of Neighbors") which implies that count to be sufficient. +**Root cause:** Bug. DREAM.3D v6.5.171 uses `while(currentLevel > m_NumberOfNeighbors)`. This condition omits the minimum level, including all propagation when the requested count is six. Both current SIMPLNX implementations use the inclusive `>=` bound. -SIMPLNX corrects this to `while(currentLevel >= m_InputValues->NumberOfNeighbors)` (`src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/BadDataNeighborOrientationCheck.cpp:129`). The fix is also explicitly documented as "BUG: Fix only checking values greater than the supplied min number of neighbors" in the merge commit of PR #1499 and in the engineer's V&V test archive README at `bad_data_neighbor_orientation_check_v2/README.md` §"Issue 1". +**Affected users:** Users of DREAM.3D v6.5.171 or DREAM3D-NX v7.0.0 through v7.4.0 whose bad voxels need propagation at the minimum neighbor level. A changed mask can change downstream results. -**Affected users:** Anyone running the filter in DREAM3D 6.5.171 with `NumberOfNeighbors < 6` on a dataset where the bottom level matters (i.e., where any bad voxel's eligible-neighbor count equals the user's `NumberOfNeighbors`). In practice this is the typical usage — the Small IN100 reconstruction pipeline (the canonical DREAM3D example) uses `NumberOfNeighbors = 4`. The 6.5.171 output left bad voxels unflipped that should have been flipped, manifesting downstream as smaller-than-expected grain reconstructions, more "rough" grain boundaries, and lower fraction of good voxels. +**Recommendation:** Trust SIMPLNX. DREAM3D-NX v7.4.1 contains the fix. The inclusive bound agrees with the parameter definition of a minimum number of neighbors. -**Recommendation:** Trust SIMPLNX. The 6.5.171 result was mathematically incorrect for the stated parameter semantics. The minimal legacy patch is a one-line change from `>` to `>=`; the root cause was proven by applying this fix (bundled with D2) to a local build of the legacy source — contact the DREAM3D team for the legacy-parity patch. +**Evidence:** PR #1499 (`ebdce592f`) changes `>` to `>=`. `Case 1.1.1` through `Case 1.6.1` check the minimum-level result. `BadDataNeighborOrientationCheckWorklist.cpp` and `BadDataNeighborOrientationCheckScanline.cpp` retain the inclusive bound. Release-source checks are recorded in `../provenance/BadDataNeighborOrientationCheckFilter.md`. --- -## BadDataNeighborOrientationCheckFilter-D2 +### BadDataNeighborOrientationCheckFilter-D2 | Field | Value | |---|---| | **Deviation ID** | `BadDataNeighborOrientationCheckFilter-D2` | | **Filter UUID** | `3f342977-aea1-49e1-a9c2-f73760eba0d3` | -| **Status** | active (latent — not observable in the V&V test suite, but real and code-evident) | +| **Status** | active | -**Symptom:** Latent. DREAM3D 6.5.171 can count a different-phase neighbor's misorientation as within tolerance if a *previous* same-phase neighbor's `w` was small, because the misorientation-threshold check sits outside the same-phase conditional and inherits the stale `w` from the prior iteration. SIMPLNX prevents this by moving the threshold check inside the same-phase conditional. +**Symptom:** A different-phase neighbor can inherit a small angle from an earlier neighbor and incorrectly increase the matching-neighbor count. -This bug is not directly observable in any of the 27 V&V test fixtures, because the D1 loop-bound bug (above) terminates the iterative-decay loop before any voxel whose count was incorrectly bumped by D2 could be flipped. The two legacy bugs cancel each other in the engineer's test inputs. +**Root cause:** Bug. DREAM.3D v6.5.171 tests the angle outside the same-phase condition in both counting and propagation. A different-phase neighbor can use the angle from an earlier neighbor. Worklist now tests the angle inside the same-phase condition. Scanline uses `isMisorientationMatch()` to reject a phase mismatch before calculating the angle. The rewrite changes the execution method but preserves this corrected rule and the D1 inclusive bound. -**Root cause:** Bug in DREAM3D 6.5.171. +**Affected users:** Users of DREAM.3D v6.5.171 or DREAM3D-NX v7.0.0 through v7.4.0 with mixed-phase neighbors. A stale angle can overcount matches at a phase boundary. -The legacy per-neighbor loop body is (`BadDataNeighborOrientationCheck.cpp:283-291`): +**Recommendation:** Trust SIMPLNX. DREAM3D-NX v7.4.1 contains the fix. Only a good neighbor of the same positive phase can count as a match. -```cpp -if(m_CellPhases[i] == m_CellPhases[neighbor] && m_CellPhases[i] > 0) -{ - w = m_OrientationOps[phase1]->getMisoQuat(q1, q2, n1, n2, n3); -} -if(w < misorientationTolerance) // <-- outside the same-phase conditional! -{ - neighborCount[i]++; -} -``` +**Evidence:** PR #1499 (`ebdce592f`) moves both angle tests inside the same-phase condition. `Case 1.2.2` checks a mixed-phase expected mask. A local proof version isolates 37 extra good voxels caused by stale angles; moving both tests removes these differences. See `vv_work:BadDataNeighborOrientationCheckFilter/algorithm_review/source_comparison.md`, `vv_work:BadDataNeighborOrientationCheckFilter/comparison/legacy_emulator_results.md`, and the release-source checks in `../provenance/BadDataNeighborOrientationCheckFilter.md`. -When the current neighbor has a different phase than the voxel, the `w = getMisoQuat(...)` assignment is skipped, and the subsequent `if(w < misorientationTolerance)` reads `w` from the *previous neighbor iteration* (or from `w`'s initial value `10000.0f` if no previous iteration matched). The previous iteration's `w` may be small (e.g., from a same-phase good neighbor with an identical orientation), in which case the comparison succeeds and the count is incorrectly bumped. +--- -SIMPLNX moves both the misorientation computation AND the increment inside the same-phase conditional (`Algorithms/BadDataNeighborOrientationCheck.cpp:105-117`): +## Part 2 — SIMPLNX defects and behavior changes -```cpp -if(cellPhases[voxelIndex] == cellPhases[neighborPoint] && cellPhases[voxelIndex] > 0) -{ - ebsdlib::QuatD quat2(quats[neighborPoint * 4], ...); - quat2.positiveOrientation(); - ebsdlib::AxisAngleDType axisAngle = orientationOps[laueClass1]->calculateMisorientation(quat1, quat2); - if(axisAngle[3] < misorientationTolerance) - { - neighborCount[voxelIndex]++; - } -} -``` +### BadDataNeighborOrientationCheckFilter-D3 -The bug is documented as "Issue 2" in the engineer's V&V test archive README at `bad_data_neighbor_orientation_check_v2/README.md`, and was bundled into PR #1499's REV cleanup. +| Field | Value | +|---|---| +| **Deviation ID** | `BadDataNeighborOrientationCheckFilter-D3` | +| **Filter UUID** | `3f342977-aea1-49e1-a9c2-f73760eba0d3` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | -**Affected users:** Anyone running the filter in DREAM3D 6.5.171 on a dataset with mixed phases adjacent to grain boundaries. The bug would manifest as voxels at phase boundaries being incorrectly flipped to "good" because they appear to have more within-tolerance neighbors than they actually do. +**Symptom:** A rounded-up angle tolerance can admit a neighbor that should fail the strict boundary comparison. -**Why not observable in V&V A/B:** The D1 loop-bound bug prevents iteration from reaching the level where the bumped count would matter. With `NumberOfNeighbors = N`, D1 stops iteration at `currentLevel = N + 1`, so a voxel with count = N (true count) or N + 1 (bumped count) cannot be flipped at the N level. To isolate D2, one would need to patch legacy 6.5.171 with just the D1 fix (without D2 fix), then run a mixed-phase fixture where a bad voxel's neighbor sequence includes a same-phase good neighbor followed by a different-phase neighbor. This is a future Phase 8 regression test addition. +**Root cause:** Bug. The earlier algorithm calculated radians with `float` and `numbers::pi_v`. DREAM.3D v6.5.171 also uses float arithmetic for this conversion. Worklist and Scanline now use double precision. The stored float32 quaternions give an angle slightly above 5°; both tolerance conversion and angle rounding affect the decision. -**Recommendation:** Trust SIMPLNX. The 6.5.171 result was mathematically incorrect. Both the D1 and D2 fixes were applied together to a local build of the legacy source for the root-cause proof (see D1). Note: applying only the D1 fix to 6.5.171 without also applying the D2 fix would UNCOVER D2 as new false-positive flips at phase boundaries — both fixes belong together. +**Affected users:** Users with misorientations close to the strict tolerance boundary. The output effect depends on the orientation calculation as well as the tolerance conversion. + +**Recommendation:** Trust SIMPLNX. Double precision reduces the conversion error without changing the strict `<` comparison. + +**Evidence:** PR #1631 (`ec22ac375`) replaces the float tolerance conversion with double precision. The expected masks for `Case 1.3.3`, `Case 1.4.3`, `Case 1.5.3` and `Case 1.6.3` keep the center bad. A local proof comparison isolates four center-voxel differences and removes them with the corrected conversion. See `vv_work:BadDataNeighborOrientationCheckFilter/algorithm_review/source_comparison.md` and the release-source checks in `../provenance/BadDataNeighborOrientationCheckFilter.md`. --- -### EbsdLib 2.4.1 CubicOps precision improvement (precision improvement; not a behavioral deviation in this filter's test data) +### BadDataNeighborOrientationCheckFilter-D4 -SIMPLNX delegates misorientation math to `ebsdlib::LaueOps::calculateMisorientation` (EbsdLib 2.4.1+); legacy 6.5.171 delegates to `OrientationLib::CubicOps::getMisoQuat` (DREAM3D 6.5.x). The modern API recovers ~0.02° of precision for cubic misorientations that lie on a 4-fold, 3-fold, or 2-fold symmetry op (replacing the precision-fragile `acos(w)` near 1 with the numerically stable `2·atan2(|v|, w)` using explicit reduced-quaternion v components). The improvement is documented in the EbsdLib 2.4.1 release notes (commit `5c8c993` on `/Users/mjackson/Workspace9/EbsdLib`, 2026-05-29). +| Field | Value | +|---|---| +| **Deviation ID** | `BadDataNeighborOrientationCheckFilter-D4` | +| **Filter UUID** | `3f342977-aea1-49e1-a9c2-f73760eba0d3` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | + +**Symptom:** A positive phase with an invalid crystal-structure value can cause an out-of-bounds orientation-operator access. + +**Root cause:** Bug. Released SIMPLNX source uses `crystalStructures[cellPhases[i]]` as an operator index without validating the crystal-structure value. DREAM.3D v6.5.171 performs the same unchecked operator lookup, and its `dataCheck()` validates array types and tuple counts without checking crystal-structure values. Both current implementations return `-54901` for an invalid non-sentinel value and skip a positive phase that resolves to `UnknownCrystalStructure`. + +**Affected users:** Users with invalid values in Crystal Structures, or a participating positive phase mapped to the unknown-structure sentinel. + +**Recommendation:** Trust SIMPLNX. Supply valid Laue indices; the unknown-structure sentinel cannot take part in an orientation match. -**Not observed as a deviation in this filter** because the engineer's test fixtures do not include any voxel pair whose misorientation lands on a cubic sym op. The improvement is real and affects other downstream filters (see `ComputeFeatureFaceMisorientationFilter` V&V cycle's D4); for `BadDataNeighborOrientationCheck` specifically, this is a transparent dependency upgrade. +**Evidence:** PR #1631 (`ec22ac375`) adds a Laue-index check and a per-voxel guard. No current fixture directly tests these rejection and skip paths. Release-source and legacy-source checks are recorded in `../provenance/BadDataNeighborOrientationCheckFilter.md`. --- -## Comparison artifacts +### BadDataNeighborOrientationCheckFilter-D5 + +| Field | Value | +|---|---| +| **Deviation ID** | `BadDataNeighborOrientationCheckFilter-D5` | +| **Filter UUID** | `3f342977-aea1-49e1-a9c2-f73760eba0d3` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was affected. | +| **Fixed in** | The DREAM3D-NX release after v7.4.2 | + +**Symptom:** A bad voxel with an invalid phase can cause an out-of-bounds read from Crystal Structures. + +**Root cause:** Bug. DREAM3D-NX v7.4.2 reads `crystalStructures[cellPhases[voxelIndex]]` before checking the phase range. Earlier DREAM3D-NX releases and DREAM.3D v6.5.171 also read through an unchecked phase index. Worklist and Scanline now check the phase before the lookup. Nonpositive phases do not participate; a positive phase beyond the array returns `-54902`. -Verification fixtures + comparison results are at `/Users/mjackson/Workspace9/DREAM3D_Data/TestFiles/bad_data_neighbor_orientation_check_v2/`: +**Affected users:** Users with phase values that do not identify an entry in Crystal Structures on a bad voxel. -- `case_*/case_*_*/case_*_*_cell_arrays.csv` — 27 CSV files, one per algorithmic case. Generated from engineer's hand-derived fixtures per `test_design.md`. -- `case_*/case_*_*/6_5_case_*_*_input.json` — 27 legacy DREAM3D pipelines that generate v7.0 `.dream3d` input + run `BadDataNeighborOrientationCheck` + write output. -- `vv_comparison/output_legacy/6_5_171_case_*.dream3d` — 27 legacy outputs from the official 6.5.171 PipelineRunner (`/Users/mjackson/Applications/DREAM3D.app/Contents/bin/PipelineRunner`). -- `bad_data_neighbor_orientation_check_v2/test_design.md` — engineer's hand-derived expected outputs (the Class 1 oracle SIMPLNX is verified against in Phase 6). -- `bad_data_neighbor_orientation_check_v2/README.md` — engineer's documentation of Issues 1 and 2. +**Recommendation:** Trust SIMPLNX. Correct the phase or supply the corresponding ensemble entry before processing that voxel. -Comparison script (saved at `/tmp/diff_legacy_vs_simplnx.py`) extracts SIMPLNX expected output from the inline `expectedMask` arrays in `BadDataNeighborOrientationCheckTest.cpp` and diffs against the 6.5.171 outputs. Re-runnable. +**Evidence:** PR #1568 (`4ec21e93f`) adds phase bounds checks. `Phase Index Bounds` checks the bad-voxel error and permits an invalid phase on an already good voxel. Release-source and legacy-source checks are recorded in `../provenance/BadDataNeighborOrientationCheckFilter.md`. diff --git a/src/Plugins/OrientationAnalysis/vv/deviations/CAxisSegmentFeaturesFilter.md b/src/Plugins/OrientationAnalysis/vv/deviations/CAxisSegmentFeaturesFilter.md index 4f1a12547d..0d4731380c 100644 --- a/src/Plugins/OrientationAnalysis/vv/deviations/CAxisSegmentFeaturesFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/deviations/CAxisSegmentFeaturesFilter.md @@ -1,95 +1,201 @@ -# Deviations from DREAM3D 6.5.171: CAxisSegmentFeaturesFilter +# Deviations from DREAM.3D v6.5.171: CAxisSegmentFeaturesFilter -This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent. - -Entries are referenced by stable ID (`CAxisSegmentFeaturesFilter-D`) from the V&V report and from public migration guidance. The ID is stable across renames; the Filter UUID field is the permanent cross-reference anchor. +Part 1 lists current differences from DREAM.3D v6.5.171 (D2 and D3). Part 2 lists fixed SIMPLNX defects (D1 and D4 through D9). Both parts use the stable `CAxisSegmentFeaturesFilter-D` sequence; existing IDs are unchanged. --- -## CAxisSegmentFeaturesFilter-D1 +## Part 1 — Differences from DREAM.3D v6.5.171 + +### CAxisSegmentFeaturesFilter-D2 | Field | Value | |---|---| -| **Deviation ID** | `CAxisSegmentFeaturesFilter-D1` | +| **Deviation ID** | `CAxisSegmentFeaturesFilter-D2` | | **Filter UUID** | `9fe07e17-aef1-4bf1-834c-d3a73dafc27d` | -| **Status** | active in **DREAM3D-NX 7.4.1** only; Fixed in **DREAM3D-NX 7.4.2** | +| **Status** | active | -**Symptom:** PR #1466 (2025-11-14) introduced this, so **7.4.1 (2026-03-23) is the only affected release** — 7.4.0 (2025-10-27) and earlier predate it. In affected builds the filter could report one extra (empty) feature, shift every FeatureId up by one, or grow a feature from a masked-out / unindexed voxel — whenever voxel 0 of the image was not a legitimate seed. 6.5.171 never exhibits this. +**Symptom:** Raw FeatureIds can differ between DREAM.3D v6.5.171 and SIMPLNX even when the segmentation partitions agree. -**Root cause:** Bug (SIMPLNX). The shared driver `src/simplnx/Utilities/SegmentFeatures.cpp::execute()` started the flood fill from the raw index `seed = 0` without calling `getSeed()`, so the first seed was neither validated against the mask/phase requirements nor stamped with its FeatureId. Legacy `SegmentFeatures::execute()` obtains every seed — including the first — from `getSeed()`. Restored in this V&V cycle (`seed = getSeed(gnum, nextSeed)` before the loop); the fix also applies to `EBSDSegmentFeatures` and `ScalarSegmentFeatures`, which share the driver. Pinned by the `Class 1 Analytical (Mask Excludes Voxel 0)` and `Execute Error - No Features Found (-87000)` test cases. +**Root cause:** Algorithmic choice. DREAM.3D v6.5.171 sets `m_RandomizeFeatureIds` to true and seeds the generator from the clock; its pipeline reader exposes no randomization control. SIMPLNX defaults to no randomization. Its optional shuffle uses `std::mt19937_64::default_seed` in `src/simplnx/Utilities/ClusteringUtilities.cpp` and leaves background ID zero fixed. -**Affected users:** DREAM3D-NX 7.4.1 users whose datasets have a masked-out, unindexed, or already-owned cell at linear index 0 — common in EBSD scans with a mask. Legacy 6.5.171 users are unaffected. +**Affected users:** Users who compare numeric labels across applications or repeated legacy runs. -**Recommendation:** Trust SIMPLNX at or after this V&V commit (which agrees with 6.5.171). Results from affected intermediate SIMPLNX builds on masked data should be regenerated. +**Recommendation:** Trust SIMPLNX. Compare partitions when label numbers are not the quantity of interest. + +**Evidence:** On 2026-09-17, repeated stock DREAM.3D v6.5.171 runs changed raw labels and preserved the partition. A local proof version with randomization disabled matched DREAM3D-NX v7.4.2 raw labels on the six valid shared Image cases. See `vv_work:CAxisSegmentFeaturesFilter/comparison/comparison_results.md`. The current `Class 4 Invariants (RandomizeFeatureIds)` test checks partition preservation and repeatability. --- -## CAxisSegmentFeaturesFilter-D2 +### CAxisSegmentFeaturesFilter-D3 | Field | Value | |---|---| -| **Deviation ID** | `CAxisSegmentFeaturesFilter-D2` | +| **Deviation ID** | `CAxisSegmentFeaturesFilter-D3` | | **Filter UUID** | `9fe07e17-aef1-4bf1-834c-d3a73dafc27d` | | **Status** | active | -**Symptom:** FeatureIds from 6.5.171 are a different (random) labeling on every run, while SIMPLNX produces the same FeatureIds on every run; the two versions never produce bit-identical FeatureIds arrays. +**Symptom:** SIMPLNX rejects participating non-hexagonal phases with `-8363` and positive phase indices outside Crystal Structures with `-8364`; DREAM.3D v6.5.171 segments these inputs. + +**Root cause:** Algorithmic choice. The legacy grouping calculation does not read Crystal Structures. SIMPLNX checks each participating phase against Hexagonal_High or Hexagonal_Low and validates the ensemble index. Masked cells and cells with nonpositive phases do not participate. The supported scientific domain is hexagonal c-axis segmentation. -**Root cause:** Algorithmic choice. 6.5.171 hard-codes `m_RandomizeFeatureIds = true` and seeds its RNG from the wall clock (`CAxisSegmentFeatures.cpp::initializeVoxelSeedGenerator`), and the option is not exposed as a pipeline parameter — legacy output labeling is irreproducible by construction. SIMPLNX exposes `Randomize Feature Ids` as a parameter (default `false`) and, when enabled, uses a fixed-seed `std::mt19937_64` (`ClusterUtilities::RandomizeFeatureIds`), so output is deterministic either way. The segmentation *partition* (which cells share a feature) is unaffected; the A/B runs (2026-07-22, rerun with the 3-D fixture 2026-07-24, `vv/comparisons/CAxisSegmentFeaturesFilter/`) matched partitions exactly on all five fixtures. +**Affected users:** Users with mixed phases or invalid phase indices. -**Affected users:** Anyone diffing raw FeatureIds arrays between versions or between two 6.5.171 runs; downstream statistics keyed by feature (sizes, misorientations) are invariant to the labeling. +**Recommendation:** Trust SIMPLNX. Mask non-hexagonal cells and correct invalid phase indices before segmentation. -**Recommendation:** Trust SIMPLNX. Deterministic labeling is strictly more reproducible; compare segmentations at the partition level when validating against legacy runs. +**Evidence:** `Execute Error - Non-Hexagonal Crystal Structure (-8363)` and `Execute Error - Phase Out of Ensemble Bounds (-8364)` check the current errors. The release comparison records the corresponding legacy outputs as TC9 and TC7 in `vv_work:CAxisSegmentFeaturesFilter/comparison/comparison_results.md`. --- -## CAxisSegmentFeaturesFilter-D3 +## Part 2 — SIMPLNX defects and behavior changes + +### CAxisSegmentFeaturesFilter-D1 | Field | Value | |---|---| -| **Deviation ID** | `CAxisSegmentFeaturesFilter-D3` | +| **Deviation ID** | `CAxisSegmentFeaturesFilter-D1` | | **Filter UUID** | `9fe07e17-aef1-4bf1-834c-d3a73dafc27d` | | **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.4.1. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | -**Symptom:** On data containing non-hexagonal phases that participate in segmentation, 6.5.171 silently produces a segmentation; SIMPLNX fails with error `-8363` (and `-8364` for phase values with no CrystalStructures entry). +**Symptom:** A masked or unindexed first cell could create an empty feature or start growth from an ineligible cell. -**Root cause:** Algorithmic choice (deliberate SIMPLNX guard). The c-axis is only a physically meaningful unique axis for hexagonal (6/m, 6/mmm) Laue classes, but the c-axis math itself never consults the crystal structure — legacy computes the [001] misalignment for cubic/other phases and returns scientifically meaningless groupings. SIMPLNX validates that every cell that can participate in segmentation (phase > 0, not masked out) has a Hexagonal_High or Hexagonal_Low crystal structure (`CAxisSegmentFeatures.cpp::operator()`). Unindexed (phase 0) cells and masked-out cells are exempt, since they can never seed or join a feature. +**Root cause:** Bug. DREAM3D-NX v7.4.1 sets `seed = 0` before its flood-fill loop. It bypasses `getSeed()` validation and label assignment. DREAM3D-NX v7.4.2 validates the first seed with `getSeed()`. Current connected-component labeling checks every cell with `isValidVoxel()` before assigning a label. DREAM.3D v6.5.171 obtains every seed through `getSeed()`. -**Affected users:** Anyone who ran the legacy filter on multi-phase data with non-hexagonal phases — their legacy results for those phases were never meaningful. Pure-hexagonal workflows are unaffected. +**Affected users:** Users of DREAM3D-NX v7.4.1 whose first cell cannot seed a feature, including all-masked input. -**Recommendation:** Trust SIMPLNX. The error is a correctness guard; mask out non-hexagonal phases (supported) to segment only the hexagonal cells. +**Recommendation:** Trust SIMPLNX. Use DREAM3D-NX v7.4.2 or later and regenerate affected results. + +**Evidence:** PR #1685 (`025333c60`) contains the correction. `Class 1 Analytical (Mask Excludes Voxel 0)` and `Execute Error - No Features Found (-87000)` check the correction. TC3 in `vv_work:CAxisSegmentFeaturesFilter/comparison/comparison_results.md` records v7.4.1 labels `[0,2,2,0,3]` and four Active tuples, versus v7.4.2 labels `[0,1,1,0,2]` and three tuples. --- -## CAxisSegmentFeaturesFilter-D4 +### CAxisSegmentFeaturesFilter-D4 | Field | Value | |---|---| | **Deviation ID** | `CAxisSegmentFeaturesFilter-D4` | | **Filter UUID** | `9fe07e17-aef1-4bf1-834c-d3a73dafc27d` | -| **Status** | active in all releases through **7.4.1**; resolved in DREAM3D-NX **7.4.2** | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | -**Symptom:** In every released version up to and including 7.4.1, SIMPLNX rejected (error `-8363`) any dataset containing unindexed (phase 0) cells — whose `CrystalStructures[0]` entry is the conventional `999` sentinel — or masked-out non-hexagonal cells; 6.5.171 processes such datasets normally. +**Symptom:** Excluded non-hexagonal phases could cause a false rejection; later validation also rejected phase-zero cells and read outside the ensemble array. -**Root cause:** Bug (SIMPLNX). The D3 validation loop checked every cell's crystal structure, including phase-0 cells and cells excluded by the mask, neither of which can ever participate in segmentation (`getSeed` requires phase > 0 and a set mask bit; `determineGrouping` requires equal phases and a set mask bit). It also indexed `CrystalStructures[phase]` without a bounds check, so an out-of-range phase value read out of bounds instead of producing an error. Fixed by exempting phase ≤ 0 and masked-out cells and adding the `-8364` bounds error. Pinned by the `Phase 0 (Unindexed) Cells Tolerated`, `Masked Non-Hexagonal Cells Tolerated`, and `Execute Error - Phase Out of Ensemble Bounds (-8364)` test cases; the 2026-07-22 A/B run confirms post-fix parity with 6.5.171 on phase-0 data (TC4). +**Root cause:** Bug. DREAM3D-NX v7.0.0 through v7.2.2 validate every ensemble entry after zero, including unused or masked phases. DREAM3D-NX v7.4.0 and v7.4.1 instead validate every cell through `crystalStructures[currentPhaseIdx]`, without mask, nonpositive-phase or bounds guards. Thus the phase-zero rejection and unchecked phase-index read apply to v7.4.0 through v7.4.1, not the entire range. DREAM3D-NX v7.4.2 excludes masked and nonpositive-phase cells from validation. It checks phase bounds and caches validated phases. DREAM.3D v6.5.171 has no crystal-structure validation loop. -**Affected users:** Users of any released version through 7.4.1 with EBSD scans containing unindexed points — a very common case — or deliberately masked non-hexagonal phases. +**Affected users:** Users with excluded non-hexagonal phases; users of v7.4.0 through v7.4.1 with unindexed cells or invalid phase indices. -**Recommendation:** Trust SIMPLNX at or after this V&V commit; upgrade if the filter spuriously rejects data with unindexed points. +**Recommendation:** Trust SIMPLNX. Use DREAM3D-NX v7.4.2 or later; valid excluded cells no longer cause rejection. + +**Evidence:** PR #1682 (`5eb69f359`) adds the exemptions; PR #1685 (`025333c60`) adds bounds checking and cached validation. The phase-zero, masked non-hexagonal and phase-bounds tests cover these corrections. TC4, TC6 and TC7 in `vv_work:CAxisSegmentFeaturesFilter/comparison/comparison_results.md` demonstrate the v7.4.0/v7.4.1 failures and v7.4.2 results. --- -## CAxisSegmentFeaturesFilter-D5 +### CAxisSegmentFeaturesFilter-D5 | Field | Value | |---|---| | **Deviation ID** | `CAxisSegmentFeaturesFilter-D5` | | **Filter UUID** | `9fe07e17-aef1-4bf1-834c-d3a73dafc27d` | -| **Status** | active in all releases through **7.4.1**; resolved in DREAM3D-NX **7.4.2** | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | + +**Symptom:** RectGrid input passed the geometry selection and then crashed during execution. + +**Root cause:** Bug. The parameter accepts Image and RectGrid, but the algorithm fetched `ImageGeom`. The shared driver dereferenced the resulting null pointer for RectGrid. DREAM3D-NX v7.4.2 uses `IGridGeometry` for both geometry types. The DREAM.3D v6.5.171 filter parameters require Image geometry; RectGrid is not a supported path. + +**Affected users:** Users who select RectGrid input in DREAM3D-NX v7.0.0 through v7.4.1. + +**Recommendation:** Trust SIMPLNX. Use DREAM3D-NX v7.4.2 or later for RectGrid segmentation. + +**Evidence:** PR #1685 (`025333c60`) contains the correction. `Class 1 Analytical (RectGrid Geometry)` asserts `[1,1,2]`. TC8 in `vv_work:CAxisSegmentFeaturesFilter/comparison/comparison_results.md` records signal 11 for v7.4.0 and v7.4.1 and the expected labels for v7.4.2. + +--- + +### CAxisSegmentFeaturesFilter-D6 + +| Field | Value | +|---|---| +| **Deviation ID** | `CAxisSegmentFeaturesFilter-D6` | +| **Filter UUID** | `9fe07e17-aef1-4bf1-834c-d3a73dafc27d` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.0. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.1 | + +**Symptom:** The feature AttributeMatrix had one extra empty tuple; all-masked input could return success with no real feature. + +**Root cause:** Bug. The shared driver initialized `gnum` to 1, incremented it after each feature and stored `m_FoundFeatures = gnum`. The filter then allocated `m_FoundFeatures + 1` tuples. DREAM3D-NX v7.4.1 stores `gnum - 1`; its separate first-seed defect is D1. Current labeling counts resolved real features. DREAM.3D v6.5.171 resizes its feature matrix only when it finds a valid seed and rejects a matrix containing background only. + +**Affected users:** Users of DREAM3D-NX v7.0.0 through v7.4.0 who use feature tuple counts or depend on the no-feature error. + +**Recommendation:** Trust SIMPLNX. Use DREAM3D-NX v7.4.2 or later to obtain both the counter correction and the D1 correction. + +**Evidence:** PR #1466 (`3d1cc153a`) contains the counter correction. TC1 in `vv_work:CAxisSegmentFeaturesFilter/comparison/comparison_results.md` records four real features but six Active tuples in v7.4.0; five tuples are correct. Current analytical fixtures call `CheckActiveArray()` to enforce N + 1 tuples. + +--- + +### CAxisSegmentFeaturesFilter-D7 + +| Field | Value | +|---|---| +| **Deviation ID** | `CAxisSegmentFeaturesFilter-D7` | +| **Filter UUID** | `9fe07e17-aef1-4bf1-834c-d3a73dafc27d` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | + +**Symptom:** Cell arrays with equal tuple counts but fewer tuples than geometry cells could pass preflight and cause an out-of-bounds traversal. + +**Root cause:** Bug. Preflight compared the selected arrays with each other but not with the geometry. DREAM3D-NX v7.4.2 checks the geometry count (`-652`) and the CellData count (`-653`). It uses the CellData shape for FeatureIds. DREAM.3D v6.5.171 also compares only array tuple counts; its driver derives neighbor indices from geometry dimensions. SIMPL `DataContainerArray::validateNumberOfTuples()` does not check geometry size. + +**Affected users:** Users with malformed cell arrays or an inconsistent cell AttributeMatrix. + +**Recommendation:** Trust SIMPLNX. Supply exactly one tuple per geometry cell; current preflight rejects inconsistent data. + +**Evidence:** PR #1685 (`025333c60`) contains the correction. `Preflight Error - Cell Arrays Smaller Than Geometry (-652)` uses nine tuples for ten cells. `Preflight Error - Cell AttributeMatrix Smaller Than Geometry (-653)` checks a nine-tuple output parent with ten input tuples. Both guards are absent through v7.4.1 and present in v7.4.2. + +--- + +### CAxisSegmentFeaturesFilter-D8 + +| Field | Value | +|---|---| +| **Deviation ID** | `CAxisSegmentFeaturesFilter-D8` | +| **Filter UUID** | `9fe07e17-aef1-4bf1-834c-d3a73dafc27d` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | + +**Symptom:** Quats in a sibling AttributeMatrix caused execution to dereference a missing FeatureIds array. + +**Root cause:** Bug. Preflight created FeatureIds in the geometry cell matrix, but execution used `QuatsArrayPath.replaceName(...)`. DREAM3D-NX v7.4.2 uses `gridGeom.getCellDataPath()` in both preflight and execution. DREAM.3D v6.5.171 creates and uses FeatureIds beside Quats, so its paths agree. + +**Affected users:** Users whose selected Quats array is outside geometry CellData. + +**Recommendation:** Trust SIMPLNX. Current execution writes FeatureIds to the same path declared by preflight. + +**Evidence:** PR #1685 (`025333c60`) contains the correction. `Class 1 Analytical (Quats Outside Cell AttributeMatrix)` asserts `[1,1,2]` in CellData with Quats and phases in OrientationData. The mismatched path is present in every release through v7.4.1 and corrected in v7.4.2. + +--- + +### CAxisSegmentFeaturesFilter-D9 + +| Field | Value | +|---|---| +| **Deviation ID** | `CAxisSegmentFeaturesFilter-D9` | +| **Filter UUID** | `9fe07e17-aef1-4bf1-834c-d3a73dafc27d` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | + +**Symptom:** Cancellation during a valid segmentation could produce error `-87000` instead of a clean return. -**Symptom:** SIMPLNX accepts a RectGrid geometry as input (6.5.171 accepts only Image geometry); in every released version up to and including 7.4.1, selecting a RectGrid geometry passed preflight and then crashed at execute. +**Root cause:** Bug. The released driver returned on cancellation before assigning `m_FoundFeatures`, which starts at zero. The caller then applied the no-feature check. DREAM3D-NX v7.4.2 returns on cancellation before that check. DREAM.3D v6.5.171 sizes Active when it finds each valid seed and checks that array size after stopping; it does not use this deferred feature counter. -**Root cause:** Bug (SIMPLNX). The algorithm fetched the geometry with a stale `getDataAs()` cast, returning a null pointer for RectGrid input, which the shared segmentation driver dereferenced. Fixed to `getDataAs()`, matching the parameter's allowed types and the sibling EBSD/Scalar segmentation algorithms. Pinned by the `Class 1 Analytical (RectGrid Geometry)` test case. +**Affected users:** Users who cancel segmentation after an eligible seed exists. -**Affected users:** Users of any released version through 7.4.1 segmenting RectGrid data — e.g., regularized serial-sectioning data. Legacy users are unaffected (the capability does not exist in 6.5.171). +**Recommendation:** Trust SIMPLNX. DREAM3D-NX v7.4.2 and current code return before reporting a no-feature error when canceled. -**Recommendation:** Trust SIMPLNX at or after this V&V commit. +**Evidence:** PR #1685 (`025333c60`) contains the correction. The cancellation return is absent in the algorithm on release branches through v7.4.1 and present in v7.4.2. Current code keeps the guard after `executeCCL()`. No test in `CAxisSegmentFeaturesTest.cpp` injects cancellation; report path 11 records that gap. diff --git a/src/Plugins/OrientationAnalysis/vv/deviations/ComputeAvgCAxesFilter.md b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeAvgCAxesFilter.md index c47234a415..9a668f1268 100644 --- a/src/Plugins/OrientationAnalysis/vv/deviations/ComputeAvgCAxesFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeAvgCAxesFilter.md @@ -1,125 +1,113 @@ -# Deviations from DREAM3D 6.5.171: ComputeAvgCAxesFilter +# Deviations from DREAM.3D v6.5.171: ComputeAvgCAxesFilter -This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent. +Part 1 lists the output differences from DREAM.3D v6.5.171 (D1 and D2). Part 2 lists fixed SIMPLNX defects (D3 through D5). Both parts use one stable ID sequence. The Filter UUID is the permanent cross-reference anchor. -Entries are referenced by stable ID (`ComputeAvgCAxesFilter-D`) from the V&V report and from public migration guidance. The ID is stable across renames; the Filter UUID field is the permanent cross-reference anchor. - -**Headline result of the comparison** (all values float32, 11-cell hand-built fixture; expected once the normalize fix is re-applied to the local build of the legacy source): +--- -| Feature | 6.5.171 (official) | Patched legacy (post-normalize) | SIMPLNX (post-normalize) | Notes | -|---|---|---|---|---| -| **0** | **(0, 0, 0)** | **(NaN, NaN, NaN)** | **(NaN, NaN, NaN)** | **placeholder — D1 (counter==0)** | -| 1 | (0, 0, 1.000) | (0, 0, 1.000) | (0, 0, 1.000) | identity quaternion, all match | -| 2 | (0, 0.866, 0.500) | (0, 0.866, 0.500) | (0, 0.866, 0.500) | +60° X, all match | -| 3 | (0, 0, 1.000) | (0, 0, 1.000) | (0, 0, 1.000) | 3 aligned cells, all match | -| 4 | (0, 0, 1.000) | (0, 0, 1.000) | (0, 0, 1.000) | antipodal pair, all match | -| **5** | **(0, 0, 1.000)** | **(NaN, NaN, NaN)** | **(NaN, NaN, NaN)** | **no cells assigned — D1** | -| **6** | **(0, 0, 1.000)** | **(NaN, NaN, NaN)** | **(NaN, NaN, NaN)** | **all cells non-hex — D1** | -| **7** | **(0, 0, 0.667)** | **(0, 0.866, 0.500)** | **(0, 0.866, 0.500)** | **antipodal-flip boundary, post-normalize — D2** | +## Part 1 — Differences from DREAM.3D v6.5.171 -SIMPLNX is expected to be bit-identical to the patched local build of the legacy source (once the patch's normalize step lands). 6.5.171 differs at F0/F5/F6 (D1) and F7 (D2). The Phase-7 changes applied surgically to the legacy build (precision + matrix-math style + counter==0 → NaN at finalize + final per-feature normalize) explain all 4 deviations completely. +### ComputeAvgCAxesFilter-D1 ---- +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeAvgCAxesFilter-D1` | +| **Filter UUID** | `453cdb58-7bbb-4576-ad5e-f75a1c54d348` | +| **Status** | active | -## ComputeAvgCAxesFilter-D1 +**Symptom:** SIMPLNX writes three NaN components for a feature with no hexagonal contribution. DREAM.3D v6.5.171 leaves feature zero at `(0,0,0)` and writes `(0,0,1)` for an empty positive feature. -| Field | Value | -|------------------|----------------------------------------| -| **Deviation ID** | `ComputeAvgCAxesFilter-D1` | -| **Filter UUID** | `453cdb58-7bbb-4576-ad5e-f75a1c54d348` | -| **Status** | active | +**Root cause:** Algorithmic choice. DREAM.3D v6.5.171 `FindAvgCAxes` has no Cell Phases or Crystal Structures inputs. It includes every cell with a positive FeatureId. Its final loop starts at feature 1 and supplies `(0,0,1)` when the count is zero. SIMPLNX accepts only Hexagonal_High and Hexagonal_Low contributions, finalizes from feature zero, and writes NaN when the count is zero. The current code also preserves hexagonal contributions when a feature contains both hexagonal and non-hexagonal cells. DREAM3D-NX v7.0.0 through v7.4.1 wrote NaN into the sum when a non-hexagonal cell occurred. DREAM3D-NX v7.4.2 decides whether to write NaN during finalization. -**Symptom:** For any feature `i` whose `counter[i] == 0` at the final computation — meaning zero hex-phase voxels contributed to the feature — SIMPLNX writes `(NaN, NaN, NaN)` into that feature's `AvgCAxes` slot. Legacy DREAM3D 6.5.171 instead falls into a "counter==0 rescue" branch and writes `(0, 0, 1)` — a confusingly valid-looking c-axis output for a feature with no contributing data. +**Affected users:** Users with empty features, non-hexagonal phases, or mixed-phase features. Downstream calculations must handle NaN values. -This symptom manifests in three distinct scenarios: +**Recommendation:** Trust SIMPLNX for hexagonal C-axis statistics. Exclude features with NaN averages from downstream direction calculations. -| Scenario | Example in V&V dataset | SIMPLNX | 6.5.171 | -|---|---|---|---| -| Placeholder feature 0 (never referenced by any cell) | F0 | NaN | (0, 0, 0) — initial state, untouched by legacy finalize loop | -| Feature has no cells assigned | F5 — declared in CellFeatureData with 8 tuples but no cell has `FeatureId == 5` | NaN | (0, 0, 1) — rescue fires | -| Feature has cells but all are non-hex | F6 — sole cell of F6 is Cubic_High phase, skipped in cell loop, counter never incremented | NaN | (0, 0, 1) — rescue fires | +**Evidence:** PR #1631 (`ec22ac375`) moved the NaN decision to finalization. DREAM.3D tag `v6.5.171`, `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/FindAvgCAxes.cpp`, compared with `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeAvgCAxes.cpp`. `Class 1 Oracle (hand-built dataset)` checks NaN values for F0 (no assigned cells), F5 (empty positive feature), and F6 (one identity cell marked Cubic_High). The legacy source leaves F0 at zero and writes `(0,0,1)` for F5 and F6. The fixture does not contain a feature with both hexagonal and non-hexagonal cells. -(For F0 specifically, legacy 6.5.171's finalize loop starts at index 1 and never visits feature 0; the array slot stays at its zero-initialized state. SIMPLNX's finalize loop starts at index 0 and writes NaN.) +--- -**Root cause:** **Algorithmic choice** during the SIMPLNX port. The refactored algorithm consolidates "no valid contributions" handling at the per-feature finalize loop: +### ComputeAvgCAxesFilter-D2 -```cpp -// Cell loop: non-hex cells skip without writing anything -if(currentCrystalStructure != ebsdlib::CrystalStructure::Hexagonal_High && - currentCrystalStructure != ebsdlib::CrystalStructure::Hexagonal_Low) -{ - continue; -} -cellCount[currentFeatureId]++; +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeAvgCAxesFilter-D2` | +| **Filter UUID** | `453cdb58-7bbb-4576-ad5e-f75a1c54d348` | +| **Status** | active | -// ... per-cell accumulation ... +**Symptom:** SIMPLNX returns a unit average C-axis. DREAM.3D v6.5.171 returns an unnormalized mean. Rounding near a zero dot product can also change the selected direction. -// Per-feature finalize loop (starts at index 0): -if(cellCount[currentFeatureId] == 0) -{ - avgCAxes[cAxesIndex] = NAN; - avgCAxes[cAxesIndex + 1] = NAN; - avgCAxes[cAxesIndex + 2] = NAN; -} -else -{ - // divide by cellCount, then normalize -} -``` +**Root cause:** Precision and algorithmic choice. DREAM.3D v6.5.171 uses float orientation calculations and float sums. SIMPLNX uses double orientation matrices, vectors, and cosine calculations, but accumulates components in float32. Both implementations flip a contribution when its cosine with the running direction is negative. SIMPLNX increments the count before this decision and normalizes the final mean; DREAM.3D v6.5.171 increments it afterward and does not normalize the final mean. -The legacy DREAM3D 6.5.171 `FindAvgCAxes` instead writes `(0, 0, 1)` at the counter==0 case, and starts the finalize loop at index 1. A surgical fix applied to a local build of the legacy source retrofits both the SIMPLNX-style `counter==0 → NaN` write and the finalize-loop start-at-0 into the legacy code, producing **bit-identical** SIMPLNX output across F0, F5, and F6. +**Affected users:** Users who compare magnitudes with DREAM.3D v6.5.171, or whose inputs put the sign decision near a zero dot product. -**Affected users:** Anyone running the filter on EBSD data with one or more of: -- Multi-phase data including non-hexagonal phases (some features may end up all-non-hex) -- CellFeatureData with declared feature tuples that don't have cells assigned (sparse feature space) -- Pole-figure or texture-statistics pipelines that consume `AvgCAxes` downstream — the (0, 0, 1) legacy rescue value would silently propagate as a "real" c-axis; the SIMPLNX NaN allows correct downstream filtering. +**Recommendation:** Trust SIMPLNX for unit-direction output. At the sign boundary, use the documented unit-magnitude invariant; the test does not select one expected direction. -**Recommendation:** **Trust SIMPLNX.** Writing `NaN` is a more honest signal that the value is undefined for these features. The legacy `(0, 0, 1)` rescue was a side effect of the rescue branch design, not a meaningful crystallographic c-axis. Downstream consumers of `AvgCAxes` should expect and handle NaN appropriately (e.g., filter out NaN-valued features before computing pole figures or texture statistics). +**Evidence:** PR #1438 (`e6896714b`) introduced double orientation calculations and removed the final normalization then present in SIMPLNX. PR #1472 (`413e6fa46`) changed the EbsdLib matrix API. PR #1631 (`ec22ac375`) restored final normalization. Current code narrows each contribution before adding it to the float32 sum; the PR #1631 implementation narrowed the result of the addition. The tagged legacy source and the current algorithm contain the stated arithmetic. `Class 1 Oracle (hand-built dataset)` checks F7 magnitude and the general unit-vector invariant with tolerance `1e-5`. The last ideal F7 axis has zero dot product with the preceding sum. Keeping or flipping the last axis gives unit directions that are 60 degrees apart. The full derivation is in `../provenance/ComputeAvgCAxesFilter.md`. --- -## ComputeAvgCAxesFilter-D2 +## Part 2 — SIMPLNX defects and behavior changes -| Field | Value | -|------------------|----------------------------------------| -| **Deviation ID** | `ComputeAvgCAxesFilter-D2` | -| **Filter UUID** | `453cdb58-7bbb-4576-ad5e-f75a1c54d348` | -| **Status** | active | +### ComputeAvgCAxesFilter-D3 -**Symptom:** For features whose cell c-axes lie on the *antipodal-flip cancellation boundary* — where the running-average dot product evaluates to exactly zero in math but is precision-dependent in float32 — SIMPLNX and 6.5.171 produce different per-feature `AvgCAxes` outputs. After the Phase-7 normalize-at-finalize step, both implementations yield unit vectors, but with different magnitudes vs 6.5.171 (1.0 in SIMPLNX, 2/3 in 6.5.171) AND different directions. +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeAvgCAxesFilter-D3` | +| **Filter UUID** | `453cdb58-7bbb-4576-ad5e-f75a1c54d348` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | The DREAM3D-NX release after v7.4.2 | -In the V&V hand-built fixture this fires at F7: three cells with c-axes `(0, 0, 1)`, `(0, +√3/2, 0.5)`, `(0, -√3/2, 0.5)`. The legacy 6.5.171 float-precision path takes the "no flip" branch and produces `(0, 0, 0.667)`. The SIMPLNX double-precision-Eigen path takes the "flip fires" branch and produces `(0, 0.866, 0.500)` (post-normalize). These are genuinely different c-axis directions (60° apart in 3D), NOT hex c≡-c equivalents — both are valid "average c-axes" of the same cell set under different sign-assignment choices. +**Symptom:** A participating cell with a negative or out-of-range phase could read outside Crystal Structures. -**Root cause:** **Precision + matrix-math style**. +**Root cause:** Bug. Released algorithms use `crystalStructures[cellPhases[i]]`, or the equivalent lookup through `currentCellPhase`, without a bounds check. The current algorithm rejects a negative phase or a phase at or above the ensemble tuple count with `-76404`. It skips nonpositive FeatureIds before checking the phase. DREAM.3D v6.5.171 has no phase lookup and was not affected. -The relevant lines in `Algorithms/ComputeAvgCAxes.cpp` lines 118–122 compute the running-average reference and the antipodal-flip decision: +**Affected users:** Users whose positive-FeatureId cells contain invalid phase indices. -```cpp -float64 cosAngle = ImageRotationUtilities::CosBetweenVectors(cellCAxis, runningCAxisAvg); -if(cosAngle < 0.0) -{ - cellCAxis *= -1.0f; -} -``` +**Recommendation:** Trust SIMPLNX with the fix. Correct any participating phase identified by error `-76404`. + +**Evidence:** PR #1568 (`4ec21e93f`) adds the phase bounds check. `Phase Index Bounds` checks phase -1 and phase 2 with two ensemble tuples, on participating and background cells. The current guard is in `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeAvgCAxes.cpp`. Direct inspection of that path in every release branch from `upstream/release/v7.0.0` through `upstream/release/v7.4.2` finds the unchecked lookup. + +--- -At F7 cell 10, the mathematically exact cosAngle is zero — the cell's c-axis is perpendicular to the running average. In SIMPLNX's `Eigen::Vector3d` double-precision path the computed `cosAngle` lands at a tiny negative value, firing the flip. In legacy 6.5.171's float-only path (or in a pure-double NumPy replay), the computed cosAngle lands at a tiny positive value, NOT firing the flip. The accumulated sums diverge to genuinely different directions. +### ComputeAvgCAxesFilter-D4 -PR #1438 ("Microtexture related filter cleanup") changed the inner-loop accumulator from `float` to `Eigen::Vector3d` (double); this precision change is the proximate driver. The EbsdLib API refactor (PR #1472) replaced explicit `qu2om` + helper calls with inline `toOrientationMatrix()` — verified functionally equivalent by the matching output of the patched local build of the legacy source. The Phase-7 normalize-at-finalize step (V&V cycle) widens the deviation: pre-normalize, both implementations had magnitude 2/3 at F7; post-normalize, SIMPLNX has magnitude 1.0 while 6.5.171 still has 2/3 (since the legacy code never normalized). +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeAvgCAxesFilter-D4` | +| **Filter UUID** | `453cdb58-7bbb-4576-ad5e-f75a1c54d348` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | -**Affected users:** Anyone running the filter on EBSD data containing features whose cell-level c-axes are arranged near the antipodal-flip cancellation boundary. In practice this is rare in natural EBSD data — it requires several cells with c-axes that summed (without flip) would lie nearly orthogonal to a previously-accumulated direction. The deliberately-pathological F7 test case demonstrates the sensitivity. **Users computing pole figures or per-feature texture statistics will get different magnitudes (1.0 vs ≤ 1.0) between SIMPLNX and 6.5.171, with the SIMPLNX magnitude being more predictable across pipelines (always unit-vector).** +**Symptom:** Converted SIMPL pipelines selected a cell matrix for feature output and supplied AttributeMatrix paths where the filter required phase and crystal-structure arrays. -**Recommendation:** **Trust SIMPLNX.** The unit-vector contract matches the natural interpretation of "average c-axis direction" and is what downstream filters (`ComputeFeatureReferenceCAxisMisorientations`, `ComputeFeatureNeighborCAxisMisalignments`) need anyway (those filters compute `acos(dot(a, b))` and require unit-magnitude inputs). Where direction differs at the precision boundary, both directions are valid c-axis representatives of the underlying cells — there is no objectively "right" answer at exact cancellation. +**Root cause:** Bug. `FromSIMPLJson()` derived the feature matrix from Quats, the phase path from the FeatureIds matrix, and the crystal-structure path from the AvgCAxes matrix. The correction derives the feature matrix from the output AvgCAxes path. It places `Phases` beside FeatureIds and `CrystalStructures` in the same container's `CellEnsembleData`. DREAM.3D v6.5.171 has no SIMPL-to-SIMPLNX conversion path and was not affected. + +**Affected users:** Users who import legacy `FindAvgCAxes` pipelines into the affected DREAM3D-NX releases. + +**Recommendation:** Trust SIMPLNX with the fix. After conversion, select the phase and crystal-structure arrays manually if their names differ from the conventional names. + +**Evidence:** PR #1682 (`5eb69f359`) corrects the converted array paths. `SIMPL Backwards Compatibility` checks six arguments in the `SIMPL 6.5 (UUID)` and `SIMPL 6.4 (Filter_Name)` sections. Both fixtures use `CellData` for the input and output matrix, so they do not isolate the corrected feature-matrix selection. Direct inspection of `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeAvgCAxesFilter.cpp` in every release branch finds the wrong mappings through v7.4.1 and the correction in v7.4.2. --- -## Comparison build & library nuance +### ComputeAvgCAxesFilter-D5 + +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeAvgCAxesFilter-D5` | +| **Filter UUID** | `453cdb58-7bbb-4576-ad5e-f75a1c54d348` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.0. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.1 | + +**Symptom:** Conversion rejected the legacy AvgCAxes output path instead of extracting its array name. + +**Root cause:** Bug. `DataArrayNameFilterParameterConverter` requires a JSON string, but the legacy output parameter is a path object. `DataArrayCreationToDataObjectNameFilterParameterConverter` reads the array-name field from that object. DREAM.3D v6.5.171 has no SIMPL-to-SIMPLNX conversion path and was not affected. -Legacy DREAM3D 6.5.171 (and the patched local build of its source) uses the **built-in EbsdLib/OrientationLib** inside the DREAM3D source tree (frozen at the 6.5.171 release point, with the targeted surgical patches applied on top). SIMPLNX uses an **independent, vcpkg-installed EbsdLib** that is actively updated. Both implement Rowenhorst conventions but the underlying code differs. The patched legacy build reproduces SIMPLNX *functional behavior*, not *identical library code* — sufficient to isolate the design-choice drivers of the deviations. +**Affected users:** Users who import legacy `FindAvgCAxes` pipelines into the affected DREAM3D-NX releases. -The patched legacy build's purpose is **root-cause proof**. If every observed SIMPLNX-vs-6.5.171 difference disappears when the targeted changes are applied to the legacy code, those targeted changes are conclusively the cause. They are. +**Recommendation:** Trust SIMPLNX with the fix. D4 also requires DREAM3D-NX v7.4.2 or later for the array-path mappings. -**Comparison fixtures:** -- `/Users/mjackson/Workspace9/DREAM3D_Data/TestFiles/compute_avg_c_axis/output_legacy/6_5_171_compute_avg_c_axis.dream3d` — official DREAM3D 6.5.171 release output -- a second output file alongside it in `output_legacy/` — output of the surgically patched local build of the legacy source (pending re-run after the normalize fix) -- `output_simplnx/simplnx_compute_avg_caxes.dream3d` — SIMPLNX output -- Three-way diff report: `vv/comparisons/ComputeAvgCAxesFilter/results/three_way_comparison.txt` +**Evidence:** PR #1476 (`e45bca2a5`) corrects the output-name converter. `SIMPL Backwards Compatibility` checks the converted output name `TestArray` for both legacy fixtures. The converter implementations are in `src/simplnx/Parameters/DataObjectNameParameter.cpp`. Direct inspection of `ComputeAvgCAxesFilter.cpp` in every release branch finds the string converter through v7.4.0 and the path-object converter in v7.4.1 and v7.4.2. diff --git a/src/Plugins/OrientationAnalysis/vv/deviations/ComputeAvgOrientationsFilter.md b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeAvgOrientationsFilter.md index 46c990af02..385ece6054 100644 --- a/src/Plugins/OrientationAnalysis/vv/deviations/ComputeAvgOrientationsFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeAvgOrientationsFilter.md @@ -1,87 +1,197 @@ -# Deviations from DREAM3D 6.5.171: ComputeAvgOrientationsFilter +# Deviations from DREAM.3D v6.5.171: ComputeAvgOrientationsFilter -This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent, `FindAvgOrientations` (SIMPL UUID `bf7036d8-25bd-540e-b6de-3a5ab0e42c5f`). +Part 1 records the Rodrigues differences from `FindAvgOrientations` and the retired D1 candidate. Part 2 records SIMPLNX defects found and fixed during V&V. Both parts use one stable ID sequence; the Filter UUID is the permanent reference. -Entries are referenced by stable ID (`ComputeAvgOrientationsFilter-D`) from the V&V report and from public migration guidance. The deviations apply **only to the Rodrigues (original) averaging method**; the von Mises-Fisher and Watson methods are new in SIMPLNX and have no 6.5.171 equivalent. +Source references use `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeAvgOrientations.cpp` and the DREAM.3D `v6.5.171` file `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/FindAvgOrientations.cpp`. The provenance sidecar records release-source checks and the 2026-09-17 comparison. That comparison predates the current algorithm. -> **Status:** Empirically validated 2026-06-30 via `compare-legacy-dream3d` against the **official DREAM3D 6.5.171 release** (`~/Applications/DREAM3D.app/Contents/bin/PipelineRunner`), reading byte-identical legacy-format input shared with NX `nxrunner`. -> -> Two fixtures were run: **(A)** the realistic dataset (`ASCIIData` CSVs — 480,000 cells / 409 feature tuples / single cubic phase) and **(B)** a hand-built 3-feature fixture designed to *force* the edge-case deviations. -> -> **Headline:** on fixture A, SIMPLNX and 6.5.171 agree to float32 epsilon on all 408 **real** features (`AvgQuats` identical ≤1e-6, zero sign flips; `AvgEulerAngles` max diff 4.77e-7). The divergences are confined to the **feature-0 / unindexed-voxel handling**: **D3** (empty feature 0) on fixture A, and **D2** (FeatureId-0 voxels with phase>0) demonstrated on fixture B. **D4** is a confirmed sub-epsilon precision difference. **D1** was downgraded — it could not be made to diverge and has no observable effect (see below). Per-deviation verdicts follow. +## Part 1 — Differences from DREAM.3D v6.5.171 ---- - -## ComputeAvgOrientationsFilter-D1 — NOT A DEVIATION (defensive normalization) +### ComputeAvgOrientationsFilter-D1 | Field | Value | |---|---| | **Deviation ID** | `ComputeAvgOrientationsFilter-D1` | | **Filter UUID** | `086ddb9a-928f-46ab-bad6-b1498270d71e` | -| **Status** | **downgraded 2026-06-30 — not a deviation.** No demonstrable divergence from 6.5.171. | +| **Status** | retired 2026-06-30 | + +**Symptom:** No quaternion sign difference was observed on the compared populated features. -**What it is:** SIMPLNX appends `.getPositiveOrientation()` after normalize (`ComputeAvgOrientations.cpp:448`), canonicalizing the average quaternion into the northern hemisphere (`w ≥ 0`); legacy `FindAvgOrientations` calls only `QuaternionMathF::UnitQuaternion` (no sign canonicalization). On paper this looked like it could produce a `q` vs `−q` difference for any feature whose average lands with `w < 0`. +**Root cause:** Algorithmic choice. SIMPLNX applies `getPositiveOrientation()` after normalization. DREAM.3D v6.5.171 normalizes without that explicit call. -**Why it is not a deviation (empirical + structural, 2026-06-30):** It could not be made to diverge. The Rodrigues algorithm builds each average by symmetry-reducing every voxel toward the running average (`getNearestQuat`), so the result lands in the fundamental zone — rotation angle ≤ 180° ⇒ `w ≥ 0`. A deliberate fixture (feature with `Rz(170°)` + `Rz(200°)`, intended to push the sum to `w < 0`) was reduced identically by **both** 6.5.171 and SIMPLNX to `(0, 0, 0.0436, 0.999)` (`w > 0`) — no sign flip. Even in the only theoretical corner where `w < 0` could survive (pure-triclinic, the incremental average overshooting 180°), the two quaternions represent the **same physical orientation** (`q ≡ −q`), so there is no correctness or downstream effect. `getPositiveOrientation()` is therefore a harmless defensive canonicalization, not a behavioral difference from legacy. +**Affected users:** No affected user case was found in these fixtures. -**Affected users:** None. +**Recommendation:** Either acceptable within tolerance `1e-6` on the compared features. Quaternion signs must also be interpreted with the equivalence of `q` and `-q`. -**Recommendation:** No action. Documented here so the source difference (the extra `getPositiveOrientation()` call) is not mistaken for an unverified deviation in a future audit. +**Evidence:** The 2026-09-17 comparison reports zero sign flips on all 415 real features across four fixtures. This result applies to those fixtures; it does not prove that every possible input gives the same quaternion representative. See `vv_work:ComputeAvgOrientationsFilter/comparison/comparison_summary.md`. --- -## ComputeAvgOrientationsFilter-D2 +### ComputeAvgOrientationsFilter-D2 | Field | Value | |---|---| | **Deviation ID** | `ComputeAvgOrientationsFilter-D2` | | **Filter UUID** | `086ddb9a-928f-46ab-bad6-b1498270d71e` | -| **Status** | **active — empirically demonstrated (2026-06-30)** on a forcing fixture | +| **Status** | active | -**Symptom:** Voxels labeled `FeatureId == 0` (with `Phase > 0`) contribute to averaging in SIMPLNX and produce a computed average for feature 0; in 6.5.171 they are skipped and feature 0 is left at `(0,0,0,0)`. +**Symptom:** Positive-phase cells with FeatureId zero contribute to the SIMPLNX average. DREAM.3D v6.5.171 skips them and leaves the feature-zero quaternion at `(0,0,0,0)`. -**Empirical (2026-06-30):** Forcing fixture B — feature 0 given two phase-1 cells (identity + `Rz(90°)`). SIMPLNX computed `AvgQuats[0] = (0, 0, 0.382683, 0.92388)` (the `Rz(45°)` average); the official 6.5.171 wrote `AvgQuats[0] = (0, 0, 0, 0)` (skipped). A clear, large divergence — not sub-epsilon. (The realistic fixture A had no `FeatureId == 0` cells, so this gate is dormant on conventional EBSD data.) +**Root cause:** Algorithmic choice. The legacy accumulation condition requires both FeatureId and phase to be positive. SIMPLNX removes the FeatureId condition so an unlabeled set of orientations can use feature zero. The current Rodrigues implementation retains that rule. -**Root cause:** Algorithmic choice. Legacy gates accumulation on `m_FeatureIds[i] > 0 && m_CellPhases[i] > 0` (`FindAvgOrientations.cpp:246`). SIMPLNX gates on `currentPhase > 0` only (`ComputeAvgOrientations.cpp:412`), deliberately allowing the documented use-case of averaging an unlabeled bag of orientations all tagged `FeatureId 0` (algorithm comment lines 401–411). +**Affected users:** Users who assign valid positive phases to cells labeled FeatureId zero. -**Affected users:** Only datasets where `FeatureId 0` legitimately carries `Phase > 0` data (atypical — `FeatureId 0` is conventionally background/unindexed with `Phase 0`). For conventional data there is no observable difference. +**Recommendation:** Trust SIMPLNX when feature zero represents an orientation set to average. Set the cell phase to zero when those cells must be excluded. -**Recommendation:** Either acceptable within tolerance. For conventional data the outputs match; the SIMPLNX behavior is a strict superset supporting an additional use-case (computing the average of an unlabeled orientation set). +**Evidence:** PR #1577 introduced feature-zero averaging. On 2026-09-17, `case_B_forcing_d2_d3` gives feature zero identity and Rz90 input cells. SIMPLNX outputs Rz45, `(0,0,0.382683,0.923880)`; stock DREAM.3D v6.5.171 outputs the zero quaternion. A local proof version fixes D3 but retains the feature-zero exclusion, so it outputs identity. See `vv_work:ComputeAvgOrientationsFilter/comparison/comparison_summary.md` and `vv_work:ComputeAvgOrientationsFilter/comparison/case_B_forcing_d2_d3_per_feature.csv`. --- -## ComputeAvgOrientationsFilter-D3 +### ComputeAvgOrientationsFilter-D3 | Field | Value | |---|---| | **Deviation ID** | `ComputeAvgOrientationsFilter-D3` | | **Filter UUID** | `086ddb9a-928f-46ab-bad6-b1498270d71e` | -| **Status** | **active — empirically confirmed (2026-06-30) at feature 0** | +| **Status** | active | -**Symptom:** For a feature with zero contributing voxels, SIMPLNX writes a clean identity quaternion `(0,0,0,1)`; 6.5.171 writes garbage. **Confirmed:** feature 0 is `(0,0,0,1)` in NX and `(0,0,0,0)` in 6.5.171 — the only divergent tuple in the entire 409-tuple comparison. +**Symptom:** Empty features give invalid quaternions in DREAM.3D v6.5.171 and affected DREAM3D-NX releases. SIMPLNX now writes identity quaternions and zero Euler angles. -**Root cause:** Bug in 6.5.171, via two mechanisms. (a) The legacy init and finalize loops both run `for(i = 1; i < totalFeatures)` (`FindAvgOrientations.cpp:239,263`), so feature 0 is **never finalized** and keeps its allocation default `(0,0,0,0)` — this is what the comparison observed. (b) For a zero-count feature at index ≥ 1, the legacy finalize sets `Identity` then still executes `QuaternionMathF::ScalarDivide(avgQuats[i], counts[i])` — a divide by zero — then `UnitQuaternion`. SIMPLNX (`ComputeAvgOrientations.cpp:440–444`) iterates `for(featureId = 0)` and, on `counts == 0`, writes identity and `continue`s, handling both cases cleanly. (No zero-count feature at index ≥ 1 existed in the dataset, so mechanism (b) is source-confirmed only.) +**Root cause:** Bug. The legacy finalize loop starts at feature 1, leaving tuple zero as `(0,0,0,0)`. For a later zero-count feature it sets identity, then divides by zero. DREAM3D-NX release sources through v7.4.1 also omit tuple zero and divide empty features by zero. SIMPLNX starts finalization at zero and adds `continue` after writing identity. The current implementation uses that rule and initializes the Euler buffer to zero. -**Affected users:** Anyone whose Feature Attribute Matrix contains a feature index with no contributing voxels (feature 0 always; index ≥ 1 after feature removal/renumbering gaps), and any consumer of feature 0's `AvgQuats` in legacy. +**Affected users:** Users of DREAM.3D v6.5.171 or DREAM3D-NX v7.0.0 through v7.4.1 with empty feature tuples, including background tuple zero and gaps after feature removal. -**Recommendation:** Trust SIMPLNX. The legacy zero-count result was uninitialized/undefined (`(0,0,0,0)` or division by zero); SIMPLNX's identity quaternion is well-defined. +**Recommendation:** Trust SIMPLNX. DREAM3D-NX v7.4.2 contains the fix. Identity is a defined placeholder; a zero quaternion or NaN tuple cannot represent an orientation. + +**Evidence:** PR #1577 (`41c079dd2`) corrected empty-feature finalization. The 2026-09-17 comparison shows the zero quaternion at legacy tuple zero in all four cases. Empty nonzero tuples in cases B and C1 contain NaNs in both legacy arrays. A local proof version writes identity and zero Euler angles for these tuples and matches SIMPLNX. `Rodrigues Analytical Oracle` independently checks the corrected values. See `vv_work:ComputeAvgOrientationsFilter/comparison/comparison_summary.md`. --- -## ComputeAvgOrientationsFilter-D4 +### ComputeAvgOrientationsFilter-D4 | Field | Value | |---|---| | **Deviation ID** | `ComputeAvgOrientationsFilter-D4` | | **Filter UUID** | `086ddb9a-928f-46ab-bad6-b1498270d71e` | -| **Status** | **active — empirically confirmed (2026-06-30), sub-epsilon** | +| **Status** | active | + +**Symptom:** Quaternion and Euler components can differ in their final float32 digits. + +**Root cause:** Library and precision. DREAM.3D v6.5.171 uses `QuaternionMathF` and `OrientationTransforms::qu2eu`. SIMPLNX uses EbsdLib quaternion arithmetic and `QuaternionFType::toEuler()`. Their intermediate operations differ. + +**Affected users:** Users who require bitwise equality across applications. + +**Recommendation:** Either acceptable within absolute component tolerance `1e-6` for the compared fixtures. The measurements do not define a universal error bound. + +**Evidence:** At SIMPLNX commit `8b80721c2`, the 2026-09-17 Small IN100 comparison has 408 populated real features. Against stock DREAM.3D v6.5.171, maximum differences are `8.94e-8` for quaternions and `4.77e-7` radians for Euler angles. There are 86 Euler components in the `[1e-7,1e-6)` band, and no quaternion sign flips. See `vv_work:ComputeAvgOrientationsFilter/comparison/comparison_summary.md`. + +--- + +## Part 2 — SIMPLNX defects and behavior changes + +### ComputeAvgOrientationsFilter-D5 + +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeAvgOrientationsFilter-D5` | +| **Filter UUID** | `086ddb9a-928f-46ab-bad6-b1498270d71e` | +| **Status** | active | +| **Affected released versions** | None. DREAM3D-NX v7.0.0 through v7.4.2 were not affected. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | + +**Symptom:** A phase-zero cell could change a vMF or Watson average and prevent the single-sample shortcut. + +**Root cause:** Bug. The directional counting pass excluded phase-zero cells, but its gather loop selected every cell with the same FeatureId. The filter now applies the positive, in-range phase condition to both passes. The current resident and Scanline implementations retain that condition. DREAM.3D v6.5.171 has no directional-sampling path. + +**Affected users:** Users of development builds before DREAM3D-NX v7.4.2 with phase-zero cells inside a populated feature. No released version contained this defect. + +**Recommendation:** Trust SIMPLNX. Phase-zero cells no longer contribute to the directional average. + +**Evidence:** PR #1577 introduced the sampling mismatch; PR #1664 (`bb0cd6545`) corrected it. `vMF/Watson Ignores Phase-0 Voxels` checks single-sample and multiple-sample cases. + +--- + +### ComputeAvgOrientationsFilter-D6 + +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeAvgOrientationsFilter-D6` | +| **Filter UUID** | `086ddb9a-928f-46ab-bad6-b1498270d71e` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | + +**Symptom:** An unsupported crystal-structure value could cause an out-of-bounds orientation-operator access. + +**Root cause:** Bug. The released Rodrigues code uses the crystal-structure value as an unchecked vector index. DREAM.3D v6.5.171 does the same. The filter now checks this index in Rodrigues and the directional worker. It reports skipped input with warning `-54671`. Rodrigues excludes affected cells; directional methods skip a feature whose selected phase has an unsupported structure. + +**Affected users:** Users with invalid or unknown crystal-structure values on participating phases. + +**Recommendation:** Trust SIMPLNX. The filter excludes unsupported crystal structures and reports a warning. + +**Evidence:** PR #1645 (`4f9bbc378`) added the operator-index check; PR #1664 added warning `-54671`. `Unknown Crystal Structure and Out-Of-Range Phase Guards` checks the warning and output values. + +--- + +### ComputeAvgOrientationsFilter-D7 + +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeAvgOrientationsFilter-D7` | +| **Filter UUID** | `086ddb9a-928f-46ab-bad6-b1498270d71e` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | + +**Symptom:** A positive phase outside the Crystal Structures array could cause an out-of-bounds read. + +**Root cause:** Bug. The earlier code checked only that the phase was positive before indexing Crystal Structures. The filter now checks the upper bound in Rodrigues and directional counting and gathering. The current code excludes these cells and returns warning `-54672`. DREAM.3D v6.5.171 also uses an unchecked positive phase index. + +**Affected users:** Users whose cell phases do not identify an existing ensemble tuple. + +**Recommendation:** Trust SIMPLNX. Correct the phase values or supply the missing ensemble tuples. + +**Evidence:** PR #1664 (`bb0cd6545`) added the phase upper-bound check and warning `-54672`. `Unknown Crystal Structure and Out-Of-Range Phase Guards` checks the warning and output values. + +--- + +### ComputeAvgOrientationsFilter-D8 + +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeAvgOrientationsFilter-D8` | +| **Filter UUID** | `086ddb9a-928f-46ab-bad6-b1498270d71e` | +| **Status** | active | +| **Affected released versions** | None. DREAM3D-NX v7.0.0 through v7.4.2 were not affected. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | + +**Symptom:** The directional worker could access shared array stores concurrently without a thread-safety guarantee. + +**Root cause:** Bug. The directional implementation scheduled `VmfWatsonSamplingImpl` through `ParallelDataAlgorithm` without disabling parallel execution. The worker reads shared input stores and writes shared output stores. The current resident path calls `setParallelizationEnabled(false)`. DREAM.3D v6.5.171 has no such worker. + +**Affected users:** Users of development builds before DREAM3D-NX v7.4.2 with parallel execution enabled. No released version contained this defect. + +**Recommendation:** Trust SIMPLNX. Serial worker execution removes the concurrent store access. + +**Evidence:** PR #1577 introduced the parallel worker; PR #1645 (`4f9bbc378`) disabled parallel execution. The fixtures check numerical outputs; they do not inject a race. + +--- + +### ComputeAvgOrientationsFilter-D9 + +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeAvgOrientationsFilter-D9` | +| **Filter UUID** | `086ddb9a-928f-46ab-bad6-b1498270d71e` | +| **Status** | active | +| **Affected released versions** | None. DREAM3D-NX v7.0.0 through v7.4.2 were not affected. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | -**Symptom:** `AvgEulerAngles` (and possibly `AvgQuats` at the last ULPs) may differ from 6.5.171 at the sub-epsilon level. +**Symptom:** Cancellation did not stop the vMF/Watson worker between features. -**Root cause:** Library + precision. Legacy uses `QuaternionMathF` arithmetic and `OrientationTransforms::qu2eu` for the quaternion→Euler conversion; SIMPLNX uses EbsdLib `ebsdlib::QuatF` and `QuaternionFType::toEuler()`. Both operate in `float32`, but the differing intermediate-math implementations and quaternion→Euler routines can produce last-bit differences. +**Root cause:** Bug. The directional worker had no cancellation check in its feature loop. The current worker checks `m_Filter->getCancel()` before each feature. DREAM.3D v6.5.171 has no directional-sampling path. -**Empirical (2026-06-30):** Across all 408 real features, `AvgEulerAngles` differed by at most **4.77e-7** (122 of 1227 components in the 1e-7–1e-6 band, the rest below 1e-7; mean 3.1e-8). `AvgQuats` were identical within 1e-6. Confirms the divergence is purely float32 round-off in the two independent library code paths. +**Affected users:** Users who cancelled directional averaging in development builds before DREAM3D-NX v7.4.2. No released version contained this defect. -**Affected users:** Anyone doing bit-exact comparison of `AvgEulerAngles` between versions. Differences are at the floating-point-noise level and not materially significant for any downstream calculation. +**Recommendation:** Trust SIMPLNX. The worker stops before the next feature; this change does not interrupt an EbsdLib estimation already in progress. -**Recommendation:** Either acceptable within tolerance (≈1e-6). Neither implementation is more correct; the difference is float round-off in independent library code paths. +**Evidence:** PR #1577 introduced the worker without this check; PR #1645 (`4f9bbc378`) added cancellation checks between features. The current test file does not directly inject cancellation in the resident directional worker. diff --git a/src/Plugins/OrientationAnalysis/vv/deviations/ComputeCAxisLocationsFilter.md b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeCAxisLocationsFilter.md index cfc56f58e2..ea6614e801 100644 --- a/src/Plugins/OrientationAnalysis/vv/deviations/ComputeCAxisLocationsFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeCAxisLocationsFilter.md @@ -1,12 +1,12 @@ -# Deviations from DREAM3D 6.5.171: ComputeCAxisLocationsFilter +# Deviations from DREAM.3D v6.5.171: ComputeCAxisLocationsFilter -This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent. - -Entries are referenced by stable ID (`ComputeCAxisLocationsFilter-D`) from the V&V report and from public migration guidance. The ID is stable across renames; the Filter UUID field is the permanent cross-reference anchor. +Part 1 lists the phase-domain differences from DREAM.3D v6.5.171. Part 2 lists the fixed SIMPLNX phase-index defect. Both parts use the stable `ComputeCAxisLocationsFilter-D` sequence. The Filter UUID is the permanent cross-reference anchor. --- -## ComputeCAxisLocationsFilter-D1 +## Part 1 — Differences from DREAM.3D v6.5.171 + +### ComputeCAxisLocationsFilter-D1 | Field | Value | |---|---| @@ -14,17 +14,19 @@ Entries are referenced by stable ID (`ComputeCAxisLocationsFilter-D`) from th | **Filter UUID** | `a51c257a-ddc1-499a-9b21-f2d25a19d098` | | **Status** | active | -**Symptom:** For non-hexagonal cells, NX places NaN values whereas 6.5.171 places meaningless-but-finite computed values. +**Symptom:** SIMPLNX writes three NaNs for each non-hexagonal cell; DREAM.3D v6.5.171 calculates a vector without checking the crystal structure. + +**Root cause:** Algorithmic choice. SIMPLNX checks each cell's phase and calculates a C-axis only for Hexagonal-High or Hexagonal-Low. The DREAM.3D v6.5.171 filter rotates `[0, 0, 1]` for every quaternion and has no phase lookup. -**Root cause:** Algorithmic choice - Intentional improvement to signal to the user the that the values for those phases could not be calculated. +**Affected users:** Users with non-hexagonal cells and at least one hexagonal entry in the non-background ensemble table. -**Affected users:** Anyone who has non-hexagonal phases in their input to ComputeCAxisLocationsFilter. +**Recommendation:** Trust SIMPLNX. NaN identifies cells outside the filter's hexagonal domain. -**Recommendation:** Trust SIMPLNX - ComputeCAxisLocationsFilter's calculation is only correct for hexagonal cells. The legacy filter computes meaningless values for non-hexagonal phases. +**Evidence:** `Class 1 Oracle - Mixed hexagonal and non-hexagonal phases` in `src/Plugins/OrientationAnalysis/test/ComputeCAxisLocationsTest.cpp` checks analytical vectors and NaNs. The 2026-07-31 comparison in `vv_work:ComputeCAxisLocationsFilter/original_comparison/Legacy_Comparison.md` records 15 matching all-hexagonal vectors, 45 bit-identical float32 components, identical shared inputs and agreement with the analytical oracle at absolute tolerance `1e-7`. Its CSV and pipelines are in the same folder. The 2026-09-17 comparison of DREAM.3D v6.5.171 and DREAM3D-NX v7.4.2 in `vv_work:ComputeCAxisLocationsFilter/results/nonhex/comparison.md` and `comparison.json` records 8 bit-identical hexagonal vectors and 7 cubic cells with finite legacy vectors and SIMPLNX NaNs. The runner selection is in `vv_work:ComputeCAxisLocationsFilter/scripts/run_nonhex_matrix.py`. Both comparisons precede PR #1568 (`4ec21e93f`, 2026-09-20), which adds buffered chunks and phase-index validation; they describe the binaries used in those runs. --- -## ComputeCAxisLocationsFilter-D2 +### ComputeCAxisLocationsFilter-D2 | Field | Value | |---|---| @@ -32,17 +34,19 @@ Entries are referenced by stable ID (`ComputeCAxisLocationsFilter-D`) from th | **Filter UUID** | `a51c257a-ddc1-499a-9b21-f2d25a19d098` | | **Status** | active | -**Symptom:** When there are no hexagonal phases present, NX emits an error (-3522) whereas 6.5.171 executes. +**Symptom:** SIMPLNX returns error `-3522` when the ensemble table has no hexagonal entry after index zero; DREAM.3D v6.5.171 executes. -**Root cause:** Algorithmic choice - Intentional improvement to prevent running the filter on data where the filter is not valid. +**Root cause:** Algorithmic choice. SIMPLNX scans the ensemble table before processing cells. This check uses the ensemble entries, even if no cell refers to them. DREAM.3D v6.5.171 accepts only quaternions and does not make this check. -**Affected users:** Anyone who has no hexagonal phases in their input to ComputeCAxisLocationsFilter. +**Affected users:** Users whose non-background ensemble entries are all non-hexagonal, or whose ensemble table has no non-background entries. -**Recommendation:** Trust SIMPLNX - ComputeCAxisLocationsFilter's calculation is only correct for hexagonal cells. If there are no hexagonal cells, then the filter does no actual calculation. +**Recommendation:** Trust SIMPLNX. Supply the correct hexagonal ensemble data before computing C-axis locations. + +**Evidence:** `No hexagonal phases error` in `src/Plugins/OrientationAnalysis/test/ComputeCAxisLocationsTest.cpp` checks error `-3522` for a cubic ensemble. The 2026-09-17 `all_cubic` comparison in `vv_work:ComputeCAxisLocationsFilter/results/nonhex/comparison.md` records successful DREAM.3D v6.5.171 execution and DREAM3D-NX v7.4.2 rejection with `-3522`. This run precedes the current algorithm's buffered chunks and phase-index validation. --- -## ComputeCAxisLocationsFilter-D3 +### ComputeCAxisLocationsFilter-D3 | Field | Value | |---|---| @@ -50,12 +54,36 @@ Entries are referenced by stable ID (`ComputeCAxisLocationsFilter-D`) from th | **Filter UUID** | `a51c257a-ddc1-499a-9b21-f2d25a19d098` | | **Status** | active | -**Symptom:** NX emits an unconditional warning (-3521) in preflight which advises the user to make sure their data has hexagonal phases and emits a warning (-3523) if there are non-hexagonal phases. 6.5.171 does not emit any warning in either case. +**Symptom:** SIMPLNX emits warning `-3521` after successful preflight and warning `-3523` when the non-background ensemble table contains both hexagonal and non-hexagonal entries; DREAM.3D v6.5.171 emits neither warning. + +**Root cause:** Algorithmic choice. Preflight reminds the user of the hexagonal requirement. Execution warns about NaN output for non-hexagonal phases after checking that at least one hexagonal ensemble exists. The warnings depend on the ensemble table, not on which phases cells use. -**Root cause:** Algorithmic choice - Intentional improvement to warn the user that the filter's output is only valid for hexagonal phases. +**Affected users:** All users who pass preflight see `-3521`. Users with mixed non-background ensemble entries also see `-3523` during execution. -**Affected users:** Anyone running the filter for the preflight warning, and anyone who has mixed non-hexagonal phases in their input to ComputeCAxisLocationsFilter. +**Recommendation:** Trust SIMPLNX. Check the selected phase and crystal-structure arrays when these warnings appear. -**Recommendation:** Trust SIMPLNX - ComputeCAxisLocationsFilter's calculation is only correct for hexagonal cells. +**Evidence:** `Not all hexagonal phases warning` in `src/Plugins/OrientationAnalysis/test/ComputeCAxisLocationsTest.cpp` checks both codes and NaN output. The 2026-09-17 DREAM3D-NX v7.4.2 runs in `vv_work:ComputeCAxisLocationsFilter/results/nonhex/comparison.md` record warnings `-3521` and `-3523` for `mixed_hex_cubic`, and warning `-3521` before error `-3522` for `all_cubic`. These runs precede the current algorithm's buffered chunks and phase-index validation. --- + +## Part 2 — SIMPLNX defects and behavior changes + +### ComputeCAxisLocationsFilter-D4 + +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeCAxisLocationsFilter-D4` | +| **Filter UUID** | `a51c257a-ddc1-499a-9b21-f2d25a19d098` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | The DREAM3D-NX release after v7.4.2 | + +**Symptom:** A negative cell phase or a phase at least as large as the ensemble tuple count could cause an out-of-bounds read from Crystal Structures. + +**Root cause:** Bug. Released SIMPLNX code reads `crystalStructures[cellPhases[i]]` without checking the phase value. Its preflight checks array types, component shapes and cell tuple counts, but not the phase range. The current algorithm checks both bounds before indexing the ensemble cache and returns `-3524` with the array paths, phase value and voxel index. DREAM.3D v6.5.171 was not affected because `FindCAxisLocations` has no phase input or ensemble lookup. + +**Affected users:** Users with invalid cell phase indices when at least one non-background ensemble entry is hexagonal. + +**Recommendation:** Trust SIMPLNX. Correct each invalid phase index or supply its missing ensemble entry before execution. + +**Evidence:** Direct inspection of `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeCAxisLocations.cpp` and `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeCAxisLocationsFilter.cpp` on all ten `upstream/release/v` branches from v7.0.0 through v7.4.2 shows the unchecked lookup and no preflight range check. The PR #1568 diff adds the guard. `src/Plugins/OrientationAnalysis/test/ComputeCAxisLocationsTest.cpp::OrientationAnalysis::ComputeCAxisLocationsFilter: Phase Index Bounds` checks error `-3524` for phases -1 and 2 with two ensemble tuples. diff --git a/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFaceIPFColoringFilter.md b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFaceIPFColoringFilter.md index acecda1a44..4c23c64c36 100644 --- a/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFaceIPFColoringFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFaceIPFColoringFilter.md @@ -1,12 +1,12 @@ -# Deviations from DREAM3D 6.5.171: ComputeFaceIPFColoringFilter +# Deviations from DREAM.3D v6.5.171: ComputeFaceIPFColoringFilter -This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent (`GenerateFaceIPFColoring`). - -Entries are referenced by stable ID (`ComputeFaceIPFColoringFilter-D`) from the V&V report and from public migration guidance. The ID is stable across renames; the Filter UUID field is the permanent cross-reference anchor. +Part 1 lists differences from `GenerateFaceIPFColoring` in DREAM.3D v6.5.171 (D1, D2, D4 and D5). Part 2 lists the fixed SIMPLNX phase-bounds defect (D3). Both parts use one stable ID sequence. --- -## ComputeFaceIPFColoringFilter-D1 +## Part 1 — Differences from DREAM.3D v6.5.171 + +### ComputeFaceIPFColoringFilter-D1 | Field | Value | |---|---| @@ -14,50 +14,96 @@ Entries are referenced by stable ID (`ComputeFaceIPFColoringFilter-D`) from t | **Filter UUID** | `30759600-7c02-4650-b5ca-e7036d6b568e` | | **Status** | active | -**Symptom:** On a surface-mesh face, the **Phase-2 (second) color** differs between SIMPLNX and DREAM3D 6.5.171 in two situations: (a) when the two adjacent features use **different Laue groups** (e.g., cubic on one side, hexagonal on the other), the 6.5.171 Phase-2 color is computed with the **wrong symmetry operator**; and (b) when the **first** face label is the invalid/exterior side (`feature1 ≤ 0`) while the second is a real feature, 6.5.171 leaves the Phase-2 color **black** instead of coloring it. SIMPLNX colors both correctly. +**Symptom:** The second face side uses the wrong symmetry on a mixed-phase boundary, or stays black when the first side is exterior and phase zero has an unknown crystal structure. + +**Root cause:** bug. The second-side calculation reads the second feature's Euler angles and reverses the normal, but its crystal-structure guard and operator lookup use the first phase. For a mixed-phase face, this selects the wrong Laue operator. For an exterior first side, the guard reads the phase-zero sentinel and skips the second color. The corrected calculation uses the second phase in both lookups. -**Root cause:** **Bug in 6.5.171.** In the Phase-2 branch of `CalculateFaceIPFColorsImpl::generate`, both the crystal-structure validity guard and the IPF-operator lookup indexed `m_CrystalStructures[phase1]` instead of `m_CrystalStructures[phase2]`, even though the same branch correctly read `feature2`'s Euler angles and the flipped face normal (`GenerateFaceIPFColoring.cpp:166,175` in 6.5.171; the same lines were ported verbatim into SIMPLNX `Algorithms/ComputeFaceIPFColoring.cpp:114,123`). Two consequences follow from the single wrong index: +**Affected users:** Users of DREAM3D-NX v7.0.0 through v7.4.1 or DREAM.3D v6.5.171 with adjacent features of different crystal symmetries, or exterior faces whose first label is non-positive. The exterior-face defect also affects single-phase data. -- *Mixed-phase faces:* `ops[m_CrystalStructures[phase1]]` applies Phase 1's symmetry operators to Phase 2's orientation — the output stays inside the IPF triangle so it looks valid but is materially wrong. -- *`feature1`-invalid boundary faces:* `phase1 = 0`, so the guard reads `m_CrystalStructures[0]` (the `UnknownCrystalStructure` sentinel, 999), which is `≥ LaueGroupEnd`; the guard fails and the second color is never written, leaving it black. This affects **even single-phase datasets** — which is why the legacy Small IN100 GBCD exemplar changed when the fix was applied. +**Recommendation:** trust SIMPLNX. DREAM3D-NX v7.4.2 contains the fix. Each face side uses its own feature's phase and orientation. + +**Evidence:** `Class 1 Oracle - mixed-phase analytical` asserts all 30 output bytes on five faces, including both defect triggers. `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeFaceIPFColoring.cpp` uses `phase1` for the second side from `upstream/release/v7.0.0` through `upstream/release/v7.4.1`; v7.4.2 uses `phase2` after PR #1646 (`f5b383be6`). DREAM.3D tag `v6.5.171` has the wrong index in `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/GenerateFaceIPFColoring.cpp`. `vv_work:ComputeFaceIPFColoringFilter/legacy_comparison_summary.md` records the 2026-06-30 comparison of stock DREAM.3D v6.5.171, a local proof version with D1 corrected, and SIMPLNX with D1 corrected. Its inputs are `vv_work:ComputeFaceIPFColoringFilter/build_fixture.py` and `vv_work:ComputeFaceIPFColoringFilter/fx_6_5_171.json`. Two of four complete face-color cases match stock DREAM.3D v6.5.171; all four first-side colors match. The summary also records 120,000 changed faces among 756,474 archived Small IN100 faces, all with non-positive first labels; the other 636,474 match. That comparison uses archived colors, not a new legacy execution. These results predate the algorithm changes in PR #1646 and PR #1568 (`4ec21e93f`); they describe the 2026-06-30 binaries. + +--- -The fix (`phase1`→`phase2`) is confirmed against the Class 1 analytical oracle and matches the legacy 6.5.172 backport `1c96b3b8e`. +### ComputeFaceIPFColoringFilter-D2 -**Empirical evidence — legacy binary A/B (hand-built mixed cubic/hex mesh).** On a legacy-native -4-face fixture (authored with the `legacy_dream3d` writer; generators + pipelines archived on OneDrive under `vv_work/face_ipf/`), DREAM3D -6.5.171 reproduces the hand-derived *buggy* oracle exactly: the mixed-phase face's Phase-2 color -is cubic red `(255,0,0)` (Phase-1's operator on a hex feature) and the `feature1`-invalid boundary -face's Phase-2 color is black `(0,0,0)`. DREAM3D 6.5.172 and SIMPLNX both apply the hex operator on -those two faces (the fix). The two faces are exactly the ones predicted; cubic-side colors and the -cubic|cubic control face are identical across all three. +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeFaceIPFColoringFilter-D2` | +| **Filter UUID** | `30759600-7c02-4650-b5ca-e7036d6b568e` | +| **Status** | active | -**Empirical evidence — real data (756,474-face Small IN100 GBCD mesh).** SIMPLNX (fixed) vs the -buggy `SurfaceMeshFaceIPFColors` baked into the file (authentic 6.5.171-class output) differs on -**exactly 120,000 faces**, every one a `feature1 ≤ 0` boundary face, **zero** elsewhere (single-phase, -so only the boundary manifestation appears). This figure is **reproducible** by re-running the A/B -generators + pipelines archived on OneDrive (`vv_work/face_ipf/`); the rendered snapshots and the -prose `legacy_comparison_summary.md` were not preserved, so it is reproducible rather than archived as -a result file. The `phase1`→`phase2` fix itself is verified independently of this A/B by the Class 1 -analytical oracle, so this real-data figure is corroborating. +**Symptom:** The tested hexagonal basal direction is green `(0,255,0)` in SIMPLNX and blue `(0,0,255)` with the legacy library after correction of D1 in a local proof version. -**Affected users:** (a) Anyone coloring surface meshes of **multi-phase** microstructures where adjacent phases use different Laue groups — alpha/beta titanium, dual-phase steels, IN625 with inclusions. (b) **All** users (including single-phase) on exterior/boundary faces where the mesh ordered the exterior side as `feature1`; those faces' second color was silently black in 6.5.171. Users who only inspect the first color, or whose meshes order the real feature as `feature1`, are unaffected. +**Root cause:** library. The libraries assign different hues to the tested basal corner. The difference remains when both implementations use the correct hexagonal operator. Both outputs have a zero red channel. -**Recommendation:** **Trust SIMPLNX.** The 6.5.171 output was mathematically incorrect on the Phase-2 side; SIMPLNX applies each phase's own symmetry operators to its own orientation. DREAM3D 6.5.172 contains the same fix (`1c96b3b8e`); SIMPLNX and 6.5.172 agree on *which* operator to apply (the residual hex hue difference between them is a separate library deviation, `-D2` below). +**Affected users:** Users who compare hexagonal basal-plane IPF colors between DREAM3D-NX and DREAM.3D v6.5.171. The cubic controls in this comparison match. + +**Recommendation:** trust SIMPLNX. Use its color key when interpreting its colors. The current analytical test encodes the green basal corner; the zero-red invariant does not depend on the green/blue assignment. + +**Evidence:** `vv_work:ComputeFaceIPFColoringFilter/legacy_comparison_summary.md` records the 2026-06-30 comparison with D1 corrected in a local proof version. It separates the library hue difference from the wrong-phase defect and records the same green expectation with EbsdLib 3.0.0 and 3.1.0. --- -## ComputeFaceIPFColoringFilter-D2 +### ComputeFaceIPFColoringFilter-D4 | Field | Value | |---|---| -| **Deviation ID** | `ComputeFaceIPFColoringFilter-D2` | +| **Deviation ID** | `ComputeFaceIPFColoringFilter-D4` | +| **Filter UUID** | `30759600-7c02-4650-b5ca-e7036d6b568e` | +| **Status** | active | + +**Symptom:** SIMPLNX produces two three-component RGB arrays instead of one six-component array. + +**Root cause:** algorithmic choice. SIMPLNX separates first-side and second-side output. `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeFaceIPFColoringFilter.cpp` creates both arrays with component shape `{3}`. The DREAM.3D v6.5.171 source creates one array with six components. + +**Affected users:** Users who import pipelines or downstream scripts that expect `SurfaceMeshFaceIPFColors` to contain both sides. + +**Recommendation:** trust SIMPLNX. Read the first and second RGB arrays separately. `FromSIMPLJson()` maps the legacy output name to the first array and leaves the second output name at its default. + +**Evidence:** PR #1427 (`0b81a0d59`) separates the two output arrays. `Class 1 Oracle - mixed-phase analytical` checks both arrays; `SIMPL Backwards Compatibility` checks the converted first output name. + +--- + +### ComputeFaceIPFColoringFilter-D5 + +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeFaceIPFColoringFilter-D5` | | **Filter UUID** | `30759600-7c02-4650-b5ca-e7036d6b568e` | | **Status** | active | -**Symptom:** For a **hexagonal** feature, the IPF color of a basal-plane reference direction differs between SIMPLNX and legacy DREAM3D (both 6.5.171 and 6.5.172): SIMPLNX produces **green `(0,255,0)`** where legacy produces **blue `(0,0,255)`** (and vice-versa for the other basal corner). Cubic coloring is identical. This is unrelated to the `-D1` wrong-phase bug — it shows up on every hex IPF color, in this filter and in the sibling cell-level `ComputeIPFColors`. +**Symptom:** SIMPLNX permits PUCM and Nolze-Hielscher IPF colors in addition to TSL. + +**Root cause:** algorithmic choice. SIMPLNX passes the selected Color Key value to EbsdLib. The DREAM.3D v6.5.171 filter has no color-key parameter. `executeImpl()` maps choices 0, 1 and 2 to TSL, PUCM and Nolze-Hielscher. + +**Affected users:** Users who select a nondefault color key. TSL remains the default, with the separate D1 and D2 differences described above. + +**Recommendation:** trust SIMPLNX. Interpret output with the selected key. + +**Evidence:** PR #1631 (`ec22ac375`) adds the Color Key parameter. `ColorKey choice reaches algorithm` checks that each nondefault first-side array differs from TSL; it does not supply independent expected RGB arrays for those keys. + +--- + +## Part 2 — SIMPLNX defects and behavior changes + +### ComputeFaceIPFColoringFilter-D3 + +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeFaceIPFColoringFilter-D3` | +| **Filter UUID** | `30759600-7c02-4650-b5ca-e7036d6b568e` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was affected. | +| **Fixed in** | The DREAM3D-NX release after v7.4.2 | + +**Symptom:** A referenced positive feature phase at or above the Crystal Structures tuple count can cause an out-of-bounds read. + +**Root cause:** bug. Released algorithms index Crystal Structures with a feature phase without checking that phase against the ensemble size. Their filters validate array types and tuple counts, not phase values. Current `ValidateReferencedFeaturePhases()` in `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeFaceIPFColoring.cpp` reads feature phases and face labels, checks both sides, and returns `-24341` before coloring. It skips non-positive labels and ignores unreferenced feature phases. -**Root cause:** **Library.** SIMPLNX links EbsdLib 3.x; legacy DREAM3D links an older EbsdLib. The hexagonal Laue ops' assignment of the two basal standard-triangle corners (`[2-1-10]` vs `[10-10]`) to green/blue differs between those EbsdLib generations (equivalently, the cartesian a-axis convention / fundamental-sector reduction differs). Both assignments keep the direction in-gamut and give red-channel 0 (a basal direction is never the red c-axis corner); only the green↔blue hue assignment flips. Surfaced during the legacy A/B on the hand-built cubic/hex fixture (`vv_work/face_ipf/`): on the mixed-phase face, 6.5.171 = `(255,0,0)` (wrong phase), 6.5.172 = `(0,0,255)` (hex, legacy hue), SIMPLNX = `(0,255,0)` (hex, EbsdLib 3.x hue). Confirmed identical (`(0,255,0)`) under **both EbsdLib 3.0.0 and EbsdLib 3.1.0** — the Class 1 oracle test passes unchanged on a from-source 3.1.0 build (`NX-Com-Qt69-Vtk95-Rel-EbsdLib`). +**Affected users:** Users whose Face Labels reference a feature with a positive phase outside the ensemble range. -**Affected users:** Anyone comparing **hexagonal**-phase IPF colors between DREAM3D-NX and any legacy DREAM3D — face IPF colors and cell IPF colors alike. Cubic-only datasets are unaffected. +**Recommendation:** trust SIMPLNX. Correct invalid phase values or supply the missing ensemble tuples. The error identifies the feature, face side, phase value and available ensemble size. -**Recommendation:** **Trust SIMPLNX (EbsdLib).** EbsdLib is the canonical IPF-color authority, and its current output (3.0.0 and 3.1.0 agree: green) is the reference; legacy DREAM3D's blue is the deviation to migrate away from. The difference is a hue-assignment convention that lives in EbsdLib, not in this filter, so it applies uniformly to all hex IPF coloring (face and cell). Migration note for users: hexagonal IPF hues will change vs. legacy DREAM3D; this is expected and correct. +**Evidence:** PR #1568 (`4ec21e93f`) adds referenced-phase validation. `Phase Index Bounds` checks `-24341` for each side and success when the invalid phase belongs to an unreferenced feature. Source at every release branch from `upstream/release/v7.0.0` through `upstream/release/v7.4.2` lacks this validation. DREAM.3D tag `v6.5.171`, `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/GenerateFaceIPFColoring.cpp`, also reads Crystal Structures through an unchecked phase index. Its `dataCheckVoxel()` checks array types and tuple counts only. diff --git a/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFeatureFaceMisorientationFilter.md b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFeatureFaceMisorientationFilter.md new file mode 100644 index 0000000000..1a0a18c8f7 --- /dev/null +++ b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFeatureFaceMisorientationFilter.md @@ -0,0 +1,113 @@ +# Deviations from DREAM.3D v6.5.171: ComputeFeatureFaceMisorientationFilter + +Part 1 lists differences from `GenerateFaceMisorientationColoring` in DREAM.3D v6.5.171. Part 2 lists the SIMPLNX phase-bounds defect. D1–D4 keep the existing `ComputeFeatureFaceMisorientations-D` IDs; D5 continues the sequence under the current filter name. The Filter UUID is the permanent cross-reference anchor. + +--- + +## Part 1 — Differences from DREAM.3D v6.5.171 + +### ComputeFeatureFaceMisorientations-D1 + +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeFeatureFaceMisorientations-D1` | +| **Filter UUID** | `f3473af9-db77-43db-bd25-60df7230ea73` | +| **Status** | active | + +**Symptom:** SIMPLNX computes angles for all 11 Laue classes. DREAM.3D v6.5.171 leaves zero vectors for classes other than Hexagonal_High and Cubic_High. + +**Root cause:** Algorithmic choice. The DREAM.3D v6.5.171 source `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/GenerateFaceMisorientationColoring.cpp` selects only the two high-symmetry classes. Current `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeFeatureFaceMisorientation.cpp` accepts every index in `LaueOps::GetAllOrientationOps()`. + +**Affected users:** Users with Hexagonal_Low, Cubic_Low, Triclinic, Monoclinic, OrthoRhombic, Tetragonal_Low, Tetragonal_High, Trigonal_Low or Trigonal_High phases. + +**Recommendation:** Trust SIMPLNX. Its analytical tests distinguish valid angles from the zero vectors left by the legacy filter. + +**Evidence:** `Curated Data` checks three analytical angles for each of the 11 Laue classes. + +--- + +### ComputeFeatureFaceMisorientations-D2 + +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeFeatureFaceMisorientations-D2` | +| **Filter UUID** | `f3473af9-db77-43db-bd25-60df7230ea73` | +| **Status** | active | + +**Symptom:** SIMPLNX returns one float32 angle in degrees per face. DREAM.3D v6.5.171 returns three float32 components: the unit rotation axis multiplied by that angle. + +**Root cause:** Algorithmic choice. SIMPLNX changes the output component shape from `{3}` to `{1}`. The scalar preserves the magnitude and omits the axis direction. Its array storage is one third of the former size for the same face count. The filter's `parametersVersion()` is 2; it also changes the output-name parameter key to `misorientation_array_name` and supplies no automatic migration for the former SIMPLNX key. + +The legacy UUID `7cd30864-7bcf-5c10-aea7-d107373e2d40` maps to SIMPLNX UUID `f3473af9-db77-43db-bd25-60df7230ea73`. The conversion preserves the filter's angle-computation purpose, but consumers must adapt to the changed representation. The legacy vector norm can be compared with the new scalar on supported same-phase faces. The archived comparison uses this mapping. + +**Affected users:** Users who consume the three-component output or load a SIMPLNX version-1 pipeline. Magnitude consumers must use the scalar angle; axis consumers need a separate axis calculation. Version-1 pipelines must use the new output-name key. + +**Recommendation:** Trust SIMPLNX for angle magnitudes. Preserve a separate axis output when a downstream operation needs direction. + +**Evidence:** PR #1618 (`7cff7f2ea`) changes the output to one component and changes the output-name parameter key. `SIMPL Backwards Compatibility` checks SIMPL 6.4 and 6.5 conversion. + +--- + +### ComputeFeatureFaceMisorientations-D3 + +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeFeatureFaceMisorientations-D3` | +| **Filter UUID** | `f3473af9-db77-43db-bd25-60df7230ea73` | +| **Status** | active | + +**Symptom:** Background faces, mixed-phase faces and faces with unsupported Laue indices give NaN in SIMPLNX. DREAM.3D v6.5.171 gives zero vectors. + +**Root cause:** Algorithmic choice. SIMPLNX explicitly writes NaN when the face cannot enter the angle calculation. The legacy output is initialized to zero and remains zero for these faces. Both filters use a positive shared phase for a valid calculation. NaN distinguishes a missing angle from a genuine zero angle; it is a representation change, not a failed zero-angle calculation. + +**Affected users:** Users who aggregate angles across faces. Including legacy zero vectors changes the aggregate; SIMPLNX consumers must decide whether to exclude NaN values or propagate them. + +**Recommendation:** Trust SIMPLNX. Select faces with finite angles before calculating an aggregate that must exclude missing values. + +**Evidence:** `Curated Data` checks the two background and two mixed-phase cases; unsupported Laue indices have no direct fixture. + +--- + +### ComputeFeatureFaceMisorientations-D4 + +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeFeatureFaceMisorientations-D4` | +| **Filter UUID** | `f3473af9-db77-43db-bd25-60df7230ea73` | +| **Status** | active | + +**Symptom:** Cubic angles lose precision near zero. Before the EbsdLib fix, the SIMPLNX fixture for a 90° c-axis symmetry rotation returned about 0.0212° instead of zero. + +**Root cause:** Precision. EbsdLib computed a reduced quaternion scalar from `(qco.z() + qco.w()) / sqrt(2)` and extracted the angle with `acos`. Float32 quaternion inputs promoted to double can put this scalar slightly below one. The fix forms the reduced quaternion's vector components explicitly and uses `2*atan2(|v|, w)`. Equal components subtract to zero without amplifying the scalar rounding error. + +DREAM.3D v6.5.171 uses float intermediates and `acos` in `Source/OrientationLib/LaueOps/CubicOps.cpp`. This calculation loses precision near zero. + +**Affected users:** Users of DREAM3D-NX v7.0.0 through v7.4.1 or DREAM.3D v6.5.171 who compute cubic angles close to a symmetry-equivalent orientation or apply thresholds sensitive to small numerical differences. + +**Recommendation:** Trust SIMPLNX. DREAM3D-NX v7.4.2 contains the fix. The filter uses stable cubic angle extraction near zero. + +**Evidence:** EbsdLib commit `5c8c993`, included by SIMPLNX PR #1631 (`ec22ac375`), fixes the calculation. That PR replaces the 0.021200536° expectation in `Curated Data` with zero, with an absolute error limit of 0.000012°. The dependency declarations on `upstream/release/v7.0.0` through `upstream/release/v7.4.1` specify EbsdLib 1.0.35, 1.0.37, 1.0.38, 1.0.39, 1.0.40 or 2.1.0; their `Source/EbsdLib/LaueOps/CubicOps.cpp` uses `acos`. DREAM3D-NX v7.4.2 requires EbsdLib 3.1.2, which contains the explicit-component `atan2` fix; EbsdLib v2.4.1 also contains it. The archived comparison in `vv_work:ComputeFeatureFaceMisorientationFilter/comparison/summary.md` and `vv_work:ComputeFeatureFaceMisorientationFilter/comparison/difference_report.md` has all six comparable curated faces within 0.0001°, without the 0.0212° residual. The production cubic comparison has 1,238 of 636,474 faces outside that tolerance, with a maximum difference of 0.001925168°. + +--- + +## Part 2 — SIMPLNX defects and behavior changes + +### ComputeFeatureFaceMisorientationFilter-D5 + +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeFeatureFaceMisorientationFilter-D5` | +| **Filter UUID** | `f3473af9-db77-43db-bd25-60df7230ea73` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was affected. | +| **Fixed in** | The DREAM3D-NX release after v7.4.2 | + +**Symptom:** A positive phase outside the Crystal Structures array can cause an out-of-bounds read. + +**Root cause:** Bug. DREAM3D-NX v7.0.0 through v7.4.2 read Crystal Structures through a positive phase without checking its upper bound. DREAM.3D v6.5.171 does the same in `GenerateFaceMisorientationColoring.cpp`. The v7.4.2 algorithm also reads the front phase before checking whether the two phases match. Current `ValidateParticipatingFeaturePhases()` checks shared positive phases in blocks of 65,536 faces and returns `-98412` before the lookup. The per-face calculation now reads Crystal Structures only after the positive, same-phase condition succeeds. + +**Affected users:** Users whose referenced feature phases exceed the Crystal Structures tuple count. In v7.4.2, an invalid front phase on a mixed-phase face can also reach the lookup. + +**Recommendation:** Trust SIMPLNX. Correct an invalid participating phase or supply its ensemble entry. + +**Evidence:** `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeFeatureFaceMisorientation.cpp` on the ten release branches from `upstream/release/v7.0.0` through `upstream/release/v7.4.2` omits the participating-phase bounds validation. PR #1568 (`4ec21e93f`) adds it. `Phase Index Bounds` checks the error for shared phase 2 with only two ensemble tuples; its second section checks that phases 2 and 1 give NaN without an error. The legacy evidence is the `v6.5.171` tag's `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/GenerateFaceMisorientationColoring.cpp`. diff --git a/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFeatureFaceMisorientations.md b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFeatureFaceMisorientations.md deleted file mode 100644 index 046a366b36..0000000000 --- a/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFeatureFaceMisorientations.md +++ /dev/null @@ -1,114 +0,0 @@ -# Deviations from DREAM3D 6.5.171: ComputeFeatureFaceMisorientationFilter - -This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent (`GenerateFaceMisorientationColoring`, source at `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/GenerateFaceMisorientationColoring.{h,cpp}` in DREAM3D 6.5.171). - -Entries are referenced by stable ID (`ComputeFeatureFaceMisorientations-D`) from the V&V report and from public migration guidance. The ID is stable across renames; the Filter UUID field is the permanent cross-reference anchor. - -All four entries below are **deliberate design changes** made during the rewrite (Nathan Young, 2026-05-19), not bugs in either implementation. D4 additionally documents a related precision fix made in EbsdLib during this V&V cycle (2026-05-28). - ---- - -## ComputeFeatureFaceMisorientations-D1 - -| Field | Value | -|------------------|----------------------------------------| -| **Deviation ID** | `ComputeFeatureFaceMisorientations-D1` | -| **Filter UUID** | `f3473af9-db77-43db-bd25-60df7230ea73` | -| **Status** | active | - -**Symptom:** For faces between two features whose shared phase has a Laue class other than `Hexagonal_High` (m-3m) or `Cubic_High` (6/mmm), SIMPLNX computes a real misorientation value; legacy DREAM3D 6.5.171 wrote `0` (implicit, via the fall-through to the else branch). The nine "new" Laue classes that SIMPLNX now handles are: `Hexagonal_Low` (6/m), `Cubic_Low` (m-3), `Triclinic` (-1), `Monoclinic` (2/m), `OrthoRhombic` (mmm), `Tetragonal_Low` (4/m), `Tetragonal_High` (4/mmm), `Trigonal_Low` (-3), and `Trigonal_High` (-3m). - -**Root cause:** **Library** + **algorithmic choice**. Legacy `GenerateFaceMisorientationColoring.cpp` line 127 explicitly checks `if((m_CrystalStructures[phase1] == Ebsd::CrystalStructure::Hexagonal_High) || (m_CrystalStructures[phase1] == Ebsd::CrystalStructure::Cubic_High))` and only computes the misorientation under that guard. The legacy `OrientationLib` of that era did not have `calculateMisorientation` (or its predecessor `getMisoQuat`) implementations for the other nine Laue classes — they would have returned undefined behavior had the guard been removed. The modern EbsdLib (`vcpkg-installed/.../EbsdLib`) has `calculateMisorientation` implementations for every Laue class, so SIMPLNX dispatches by `laueIndex < m_LaueOrientationOps.size()` instead of hard-coding the two-class enumeration. - -**Affected users:** Any user with surface-mesh face data spanning grain boundaries between non-hexagonal, non-cubic-high phases. Common cases: orthorhombic systems (alpha-uranium, many minerals), monoclinic systems (gypsum, many metals at low symmetry phases), tetragonal systems (TiO2, ZrO2 below transition), trigonal systems (quartz, alpha-corundum). Users running the legacy filter on these systems received silent zeros for every triangle between matched-phase non-hex-high/non-cubic-high features. - -**Recommendation:** **Trust SIMPLNX.** The legacy filter's silent zero for unsupported Laue classes was indistinguishable from "genuine zero misorientation" (a real possibility for aligned grains) — a serious correctness ambiguity that the new explicit handling resolves. Combined with D3 (NaN for invalid faces), users get unambiguous values for every triangle. - ---- - -## ComputeFeatureFaceMisorientations-D2 - -| Field | Value | -|------------------|----------------------------------------| -| **Deviation ID** | `ComputeFeatureFaceMisorientations-D2` | -| **Filter UUID** | `f3473af9-db77-43db-bd25-60df7230ea73` | -| **Status** | active | - -**Symptom:** SIMPLNX output is a 1-component `float32` array — the misorientation angle in degrees per triangle. Legacy DREAM3D 6.5.171 wrote a 3-component `float32` array — the rotation axis (3 components) multiplied componentwise by the angle in degrees, i.e., `(w·n1, w·n2, w·n3)`. The 3-component form encodes both the rotation magnitude AND the axis direction; the 1-component form keeps only the magnitude. - -**Root cause:** **Algorithmic choice** during the rewrite. Decision attributed to Michael Jackson: "bring the output in line with other Misorientation filters in `simplnx`". Most modern misorientation filters in simplnx (KAM, Reference Misorientations, Feature Neighbor Misorientations) return a 1-component magnitude; the legacy 3-component axis·angle form was the outlier. The new form also halves the output memory footprint and simplifies most downstream uses (binning, histogram, threshold). - -**Affected users:** Any user porting a 6.5.171 pipeline that consumed the 3-component output. Common downstream uses: -- **Magnitude-only consumers** (binning for histograms, comparison against a threshold): trivial migration — read component 0 (which used to be `w·n1`) → must change to read the new 1-component (now just `w`). Many pipelines did this by extracting component 0 anyway, which would have been mathematically incorrect for the old format. -- **Axis-and-angle consumers** (visualization with the axis direction encoded in color, downstream filters that need the misorientation rotation axis): not a clean migration — they need the axis explicitly. Workaround: regenerate the axis-angle by running `LaueOps::calculateMisorientation` directly in a custom filter; or back-out the axis from the (legacy) 3-component output by dividing by the angle magnitude. - -**Recommendation:** **Either acceptable per use case.** Neither output is wrong, just different representations of the same calculation. For users needing the rotation axis: open an issue requesting a separate axis output array (could be added as a v3 of the filter via the version-bump mechanism). - ---- - -## ComputeFeatureFaceMisorientations-D3 - -| Field | Value | -|------------------|----------------------------------------| -| **Deviation ID** | `ComputeFeatureFaceMisorientations-D3` | -| **Filter UUID** | `f3473af9-db77-43db-bd25-60df7230ea73` | -| **Status** | active | - -**Symptom:** For faces where the algorithm cannot compute a meaningful misorientation — `frontFeature == 0` (background voxel on the front side), `backFeature == 0` (background on the back side), `frontPhase != backPhase` (mixed-phase boundary), or the shared phase's Laue class is out of EbsdLib's supported range — SIMPLNX writes **`NaN`**. Legacy DREAM3D 6.5.171 wrote `0` for all these cases (in the legacy code: explicit zeros for the mismatched-phase case at lines 143–145; implicit zeros for the unsupported-Laue case via fall-through). - -**Root cause:** **Bug in 6.5.171** (loose categorization), fixed by **algorithmic choice** in SIMPLNX. In 6.5.171 there is no way to differentiate between a true zero misorientation (two grains in perfect crystallographic alignment, mathematically possible) and an unprocessed face. SIMPLNX's explicit NaN makes the distinction unambiguous: - -| Output value | Meaning | -|--------------|--------------------------------------------------------------------------| -| `0.0` | Genuine zero misorientation — the two features are crystallographically aligned (or the misorientation lies on a symmetry op of the shared Laue class) | -| Positive | Computed misorientation angle in degrees | -| `NaN` | The algorithm did not compute a value for this face (any of the four reasons above) | - -**Affected users:** Anyone running pipelines that aggregated misorientation values across all faces (mean, median, histogram). In the legacy filter, the implicit zeros from unprocessed faces silently dragged the aggregate toward zero. SIMPLNX's NaN propagates correctly under standard aggregation rules (NaN-aware aggregations skip NaN; non-aware aggregations propagate to NaN). - -**Recommendation:** **Trust SIMPLNX.** The clear distinction between genuine zero and "no value" is a significant correctness improvement. Downstream consumers should use NaN-aware aggregation (`std::isnan`, `numpy.nanmean`, etc.) or pre-filter faces with NaN before aggregating. - ---- - -## ComputeFeatureFaceMisorientations-D4 - -| Field | Value | -|------------------|----------------------------------------| -| **Deviation ID** | `ComputeFeatureFaceMisorientations-D4` | -| **Filter UUID** | `f3473af9-db77-43db-bd25-60df7230ea73` | -| **Status** | active | - -**Symptom:** For cubic-phase boundaries whose true misorientation lies exactly on a cubic symmetry operator (e.g., 90° rotation about the c-axis is a 4-fold sym op of m-3m), legacy DREAM3D 6.5.171 and pre-fix SIMPLNX returned a small residual misorientation (~0.02°) instead of exactly 0°. Post-fix SIMPLNX returns exactly 0° for these cases. - -The hand-built V&V dataset exposes this at F5↔F7 (Cubic_High features with φ1=0° and φ1=90° about c): expected exactly 0° by symmetry; pre-fix observed 0.0212°; post-fix observed 0.0°. - -**Root cause:** **Precision** in EbsdLib's `CubicOps::calculateMisorientationInternal` (NOT in this filter's code). The algorithm computed `cos(half-angle)` candidates as: - -```cpp -double w_candidate_2 = (qco.z() + qco.w()) / sqrt(2); // type-2 sym op -``` - -then extracted the angle via `acos(w_candidate)`. When `AvgQuats` are stored as **float32** in the input data (the standard SIMPLNX/EbsdLib convention) and promoted to double inside the calculation: - -1. The float32 truncation of `sqrt(2)/2` is `0.7071068f` ≈ `0.70710676908...` as double (off from true `sqrt(2)/2` by ~6e-8). -2. After the symmetry-op reduction, `qco.z` and `qco.w` end up at this float32 value. -3. `(qco.z + qco.w) / sqrt(2)` (where the divisor is the precise double `sqrt(2)`) computes to `1.0 − ~1.71e-8`, NOT exactly 1.0. -4. `acos(1.0 − 1.71e-8)` is in the catastrophic-cancellation regime: the derivative `−1/sqrt(1−x²)` is unbounded as `x → 1`, so a 1.71e-8 perturbation amplifies to ~`1.85e-4` rad in the result. -5. Doubled to full angle and converted to degrees: `~0.0212°` ≈ the observed residual. - -**Fix:** Patch in EbsdLib (`Source/EbsdLib/LaueOps/CubicOps.cpp`). Compute the explicit reduced-quaternion vector components for each of the three sym-op candidates ("type 1/2/3"); pick the candidate with the largest `w`; extract the angle as `2 · atan2(|v|, w)` using `|v|` computed from the explicit reduced-quaternion components, NOT from `sqrt(1 − w²)`. The cancellation that loses precision in the legacy form is *recovered* in the new form because the explicit `v` components include subtractions of identical floats (e.g., `qco.z − qco.w` when `qco.z == qco.w`), which yield exactly 0 in IEEE 754 — regardless of the underlying float32 precision. - -The fix is mathematically equivalent for non-sym-op-aligned misorientations (both forms compute the same angle within ULP for inputs far from the cancellation boundary). It strictly improves precision for inputs on or near a sym op. - -**Affected users:** Anyone who computes misorientations on cubic phases where some grain boundaries land on or near a 4-fold (90° about c), 3-fold (120° about [111]), 2-fold (180° about face), or other cubic symmetry op. These are not pathological — many real-world cubic textures have these as systematic features (e.g., {100} fiber textures align all grains' [001] directions, so 90° about [001] is a frequent boundary). The 0.02° pre-fix residual would have caused: -- Misorientation histograms to have a spurious peak at ~0.02° that should be at 0°. -- Threshold-based grain boundary classification (e.g., "low-angle boundaries < 5°") to silently misclassify some sym-op-aligned boundaries. -- Downstream KAM and GOS calculations to include the residual. - -**Recommendation:** **Trust the fixed SIMPLNX (post-2026-05-28).** The fix is a strict improvement; all 306 EbsdLib unit tests pass post-fix; 181/189 OrientationAnalysis unit tests pass post-fix (the 8 failures are exemplar-based regression tests against pre-fix-generated exemplars, with diffs in the `1e-4` to `1e-3` range — see the V&V doc's "Downstream impact note"). Exemplar files for the 8 affected downstream tests will be regenerated at the engineer's discretion to lock in the post-fix values as the new reference. - ---- - -## Comparison build & library nuance - -This filter's V&V did NOT run a direct A/B comparison against legacy DREAM3D 6.5.171's `GenerateFaceMisorientationColoring`. The output structure is incompatible by design (D2: 3-component axis·angle vs 1-component angle), making a per-array comparison meaningless. The Class 1 oracle (hand-derived from symmetry-group analysis) serves as the verification floor; the deviation entries above document the design choices that distinguish the new filter from the legacy. diff --git a/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFeatureNeighborCAxisMisalignmentsFilter.md b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFeatureNeighborCAxisMisalignmentsFilter.md index 0b9d77b8f8..593080105b 100644 --- a/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFeatureNeighborCAxisMisalignmentsFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFeatureNeighborCAxisMisalignmentsFilter.md @@ -1,203 +1,135 @@ -# Deviations from DREAM3D 6.5.171: ComputeFeatureNeighborCAxisMisalignmentsFilter +# Deviations from DREAM.3D v6.5.171: ComputeFeatureNeighborCAxisMisalignmentsFilter -This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent (`FindFeatureNeighborCAxisMisalignments`, source at `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/FindFeatureNeighborCAxisMisalignments.{h,cpp}` in DREAM3D 6.5.171). +Part 1 lists differences from DREAM.3D v6.5.171 (D1, D4, and D6). Part 2 lists SIMPLNX initialization and diagnostic changes (D2 and D5), and the fixed phase-index defect (D7). -Entries are referenced by stable ID (`ComputeFeatureNeighborCAxisMisalignmentsFilter-D`) from the V&V report and from public migration guidance. The ID is stable across renames; the Filter UUID field is the permanent cross-reference anchor. - -## Comparison summary - -The legacy A/B comparison was performed **empirically** on 2026-06-04 against three binaries: - -- **A:** DREAM3D 6.5.171 (official release, `/Users/mjackson/Applications/DREAM3D.app/Contents/bin/PipelineRunner`) — buggy. -- **B:** A local build of the DREAM3D 6.5.171 source with the surgical divisor-bug fix applied (`/Users/mjackson/DREAM3D-Dev/DREAM3D-Build/D3D-Rel-Qt515-6_5_171/Bin/PipelineRunner`) — used only to prove the root cause — fixed. -- **C:** SIMPLNX (post-fix, `/Users/mjackson/Workspace9/DREAM3D-Build/NX-Com-Qt69-Vtk95-Rel-EbsdLib/Bin/nxrunner`) — fixed. - -All three binaries were run on the same hand-built `.dream3d` input file containing the realistic-microstructure fixture (10×10×1 ImageGeom, 6 features, mixed hex/non-hex phases, pure-Φ Bunge ZXZ rotations matching the SIMPLNX test fixture). A/B test workspace and artifacts (input `.dream3d`, 3 output `.dream3d`, per-binary pipeline files, comparison script, run results) at `/Users/mjackson/Desktop/F6_AB_Test/`. - -**Result summary:** - -- Per-pair `CAxisMisalignmentList` values: all three binaries produce identical values within float32 precision (`~1e-6°` drift between A/B and C). -- Per-feature `AvgCAxisMisalignments` for F2/F5/F6 (the bug-exposing features): A produces the predicted-buggy values; B and C both produce the analytical-correct values. -- Per-feature `AvgCAxisMisalignments` for F3 (all-non-hex neighbor list): A produces `0.0`; B and C both produce `NaN`. **Additional symptom of D1** documented under D1 below. - -SIMPLNX `ComputeFeatureNeighborCAxisMisalignments::operator()()` is a clean Port of legacy `FindFeatureNeighborCAxisMisalignments::execute()` (same per-feature outer loop, same per-neighbor inner loop, same hex-hex phase gate, same optional per-feature averaging finalize). Both implementations originally shared the divisor bug at the `hexNeighborListSize` reassignment (D1 below). The bug went undetected for the lifetime of both implementations because the existing SIMPLNX exemplar test consumed a hex-phase-only dataset, which never exercises the per-mismatch decrement branch. - -This filter is the c-axis analog of `ComputeFeatureNeighborMisorientationsFilter`. Important distinction: this filter does NOT route through `LaueOps::calculateMisorientation`. It uses Eigen for the c-axis vector math (orientation matrix → c-axis rotation → `arccos(|c1·c2|)` folded to `[0°, 90°]`), so the EbsdLib 2.4.1 precision improvement that surfaced as a deviation in F#1/F#2/F#4/F#5 of this V&V cycle **does not apply here**. +Entries use one stable ID sequence. D3 was not assigned to an entry and remains unused. The Filter UUID is the permanent cross-reference anchor. Source and release evidence are recorded in `../provenance/ComputeFeatureNeighborCAxisMisalignmentsFilter.md`. --- -## ComputeFeatureNeighborCAxisMisalignmentsFilter-D1 - -| Field | Value | -|------------------|--------------------------------------------------------------------------------------------------------------------------------------| -| **Deviation ID** | `ComputeFeatureNeighborCAxisMisalignmentsFilter-D1` | -| **Filter UUID** | `636ee030-9f07-4f16-a4f3-592eff8ef1ee` | -| **Status** | active (SIMPLNX fixed 2026-06-04; legacy 6.5.171 still has the bug — root cause proven 2026-06-04 by applying the same fix to a local build of the legacy source); SIMPLNX-side fix ships in DREAM3D-NX **7.4.2** | - -**Symptom:** Per-feature `AvgCAxisMisalignments` (output of `find_avg_misals=true` / legacy `FindAvgMisals=true`) differ between SIMPLNX (post-2026-06-04 fix) and DREAM3D 6.5.171 on any dataset where features have mixed hex/non-hex neighbor lists. The legacy result depends on the *order* in which neighbors appear in the per-feature `NeighborList`: if the last-iterated neighbor is hex-hex same-phase, the divisor used is the full neighbor-list length (incorrect); if the last neighbor is non-hex or different-phase, the divisor is decremented by 1 from the full length (the per-mismatch decrement at line 150 happens to be the last write to `hexNeighborListSize`). The legacy result is therefore correct in some cases by accident and wrong by up to `(N-K) / N` of the true value in others, where N is the neighbor count and K is the number of hex-hex same-phase neighbors. - -The bug is **not observable on the legacy SIMPLNX exemplar dataset** (`7_5_simplnx_test_file_25x50_Hex.dream3d` — hex-phase-only, so no mismatch decrements ever fire). The bug **IS observable** on the V&V `Realistic Microstructure (exposes divisor bug)` fixture, which constructs a 10×10×1 microstructure with mixed hex (F1, F2, F4, F5, F6) and cubic (F3) features. Three of the six features have neighbor lists that fire the bug (F2, F5, F6), with measured pre-fix averages of `7.500°`, `6.250°`, `2.500°` instead of the correct `10.000°`, `8.333°`, `5.000°`. - -**Additional symptom (surfaced by empirical A/B testing 2026-06-04):** for features whose entire neighbor list is non-hex (F3 in the V&V fixture), legacy 6.5.171 produces `avg = 0.0` whereas the correct behavior is `avg = NaN`. The mechanism: the buggy code reassigns `hexNeighborListSize` to the full list size at the top of each j-iteration and decrements it in the non-hex else-branch. On a 3-neighbor all-non-hex list, the iteration sequence is: j=0 (assign hex=3, decrement to 2); j=1 (assign hex=3, decrement to 2); j=2 (assign hex=3, decrement to 2). Final `hexNeighborListSize = 2`. The post-loop `if(hexNeighborListSize > 0)` branch is then true → `avg = 0.0 / 2 = 0.0` (the accumulator is 0 because no hex-match contributions were ever added). The intended behavior (executed correctly under the post-fix code) is for `hexNeighborListSize` to decrement to 0 after all three non-hex matches, falling through to the `else: avg = NaN` branch. Empirically: 6.5.171 produces `avg[F3] = 0.0`; the patched local build of the legacy source and SIMPLNX both produce `avg[F3] = NaN`. - -**Root cause:** **Bug** in both legacy DREAM3D 6.5.171 and SIMPLNX pre-fix. +## Part 1 — Differences from DREAM.3D v6.5.171 -The legacy code at `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/FindFeatureNeighborCAxisMisalignments.cpp:280` and the SIMPLNX pre-fix code at `Algorithms/ComputeFeatureNeighborCAxisMisalignments.cpp:111` both contain `hexNeighborListSize = currentNeighborList.size();` (or the legacy `hexneighborlistsize = neighborlist[i].size();`) *inside* the inner per-neighbor j-loop. The intended behavior is for `hexNeighborListSize` to start each outer-loop iteration (per feature) at the neighbor-list size and then decrement by 1 for each phase-mismatched neighbor (line 150: `hexNeighborListSize--;`). Because the reassignment happens at the *top* of each j-iteration, the decrement from the *previous* iteration is clobbered. Only the *last* j-iteration's match/mismatch state actually affects `hexNeighborListSize`: if the last neighbor is hex-hex same-phase, the assignment runs and the decrement doesn't, so the final divisor is N; if the last neighbor is non-hex or different-phase, both the assignment and the decrement run, so the final divisor is N - 1. +### ComputeFeatureNeighborCAxisMisalignmentsFilter-D1 -The SIMPLNX fix (2026-06-04) moves the `hexNeighborListSize = currentNeighborList.size();` assignment from line 111 to before the inner j-loop (alongside `currentMisalignmentList.resize(...)` at line 106), so the assignment runs once per outer-loop iteration (per feature) and the decrement is preserved across j-iterations. The result is the mathematically correct divisor: the number of hex-hex same-phase neighbors. +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeFeatureNeighborCAxisMisalignmentsFilter-D1` | +| **Filter UUID** | `636ee030-9f07-4f16-a4f3-592eff8ef1ee` | +| **Status** | active | -The bug went undetected for the lifetime of both implementations because: +**Symptom:** An excluded neighbor before an accepted neighbor reduces the calculated average below the mean of the accepted angles. A nonempty list with no accepted neighbors can produce zero instead of NaN. -1. **The legacy 6.5.171 implementation had no automated test coverage of the `FindAvgMisals=true` path on mixed-phase data.** Legacy DREAM3D's CI tested filters with default parameter values; this parameter defaults to false in many user-facing pipelines and the test infrastructure didn't sweep over both values. -2. **The SIMPLNX Port preserved the bug** and the existing exemplar dataset was hex-phase-only. The exemplar would have happily passed even on the buggy code, because the per-mismatch decrement branch never fires on hex-only data. -3. **PR #1467 ("OEM-reviewed cleanup") signed off on the buggy code.** The review focused on naming, comments, and structure — not on the inner-loop divisor invariant. -4. **The retroactive bug-triage cycle (2026-05) caught it** by source inspection. Documented in `/Users/mjackson/Desktop/bug_triage.md` as Bug #3 (sibling of Bug #2 / F#2 D1). +**Root cause:** Bug. DREAM.3D v6.5.171 resets `hexneighborlistsize` to the full list size inside each neighbor iteration. DREAM3D-NX v7.0.0 through v7.4.1 do the same. Each reset discards earlier decrements for unequal crystal structures or non-hexagonal structures. The final divisor is N if the last neighbor is accepted, or N minus one if it is excluded. DREAM3D-NX v7.4.2 initializes the divisor before the neighbor loop. The divisor then counts every accepted pair. -**Affected users:** Anyone running DREAM3D 6.5.171 or SIMPLNX pre-2026-06-04 with `FindAvgMisals=true` / `find_avg_misals=true` on data containing features with mixed hex / non-hex neighbor lists. **Production-relevant via shipping pipeline.** The reference pipeline `pipelines/EBSD_File_Processing/EBSD_Hexagonal_Data_Analysis.d3dpipeline` runs this filter with `find_avg_misals: true`. Any user running that pipeline on multi-phase EBSD data containing at least one cubic, tetragonal, or trigonal phase will have produced incorrect per-feature `AvgCAxisMisalignments` values. +**Affected users:** Users who enable averaging on neighbor lists with excluded pairs. The old value depends on neighbor order. Hexagonal-only lists of matching crystal structures do not expose the defect. -**Recommendation:** **Trust SIMPLNX (post-2026-06-04 fix).** The pre-fix per-feature `AvgCAxisMisalignments` values from both DREAM3D 6.5.171 and pre-fix SIMPLNX are mathematically incorrect for any feature with a mixed-phase neighbor list. Users migrating from 6.5.171 should expect per-feature averages to shift toward the mathematically correct value, with the shift size proportional to the fraction of phase-mismatched neighbors per feature. +**Recommendation:** Trust SIMPLNX in DREAM3D-NX v7.4.2 or later. The average uses all accepted neighbors, regardless of their order. -The root cause was proven by applying the same fix (move the `hexneighborlistsize` reassignment outside the inner loop) to `FindFeatureNeighborCAxisMisalignments.cpp` in a local build of the legacy source, bundled with the sibling `FindMisorientations` fix; contact the DREAM3D team for the legacy-parity patch. +**Evidence:** PR #1631 (`ec22ac375`) corrects the divisor. `Class 1 - Realistic Microstructure (exposes divisor bug)` checks the independent expected averages: F2 = 10°, F3 = NaN, F5 = 25/3°, and F6 = 5°. The defective divisor gives 7.5°, 0°, 6.25°, and 2.5°, respectively, by calculation; these are not legacy-run measurements. `Class 4 - Invariants` checks the mean of the non-NaN entries. `Class 1 - Mismatch Last Order` checks an ordering where the old divisor gives the correct result. The 2026-10-22 comparison records four average differences at F2, F3, F5, and F6 in `mixed_average_on`, no differences beyond 0.001° in the `mixed_average_off` control, and exact agreement with DREAM3D-NX with equal NaN positions for a local proof version in both cases (`vv_work:ComputeFeatureNeighborCAxisMisalignmentsFilter/comparison/LegacyComparison.md`; proof fixes: `vv_work:ComputeFeatureNeighborCAxisMisalignmentsFilter/notes/proof_commits.md`). --- -## ComputeFeatureNeighborCAxisMisalignmentsFilter-D2 +### ComputeFeatureNeighborCAxisMisalignmentsFilter-D4 -| Field | Value | -|------------------|------------------------------------------------------------------------------------------------------| -| **Deviation ID** | `ComputeFeatureNeighborCAxisMisalignmentsFilter-D2` | -| **Filter UUID** | `636ee030-9f07-4f16-a4f3-592eff8ef1ee` | -| **Status** | active (latent — empirically confirmed DORMANT on current SIMPLNX in-memory DataStore; may surface on future OOC backends) | +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeFeatureNeighborCAxisMisalignmentsFilter-D4` | +| **Filter UUID** | `636ee030-9f07-4f16-a4f3-592eff8ef1ee` | +| **Status** | active | -**Symptom:** Latent. When `find_avg_misals=true`, the output `AvgCAxisMisalignments` array is allocated via `CreateArrayAction` in the filter's preflight WITHOUT an explicit `fillValue` argument. Inside the algorithm, the first per-feature hex-hex match write does: +**Symptom:** Finite pair angles and averages can differ in their last digits from DREAM.3D v6.5.171. -```cpp -float32 value = avgCAxisMisalignmentPtr->getValue(featureIdx) + currentMisalignmentList[j]; -avgCAxisMisalignmentPtr->setValue(featureIdx, value); -``` +**Root cause:** Precision and library. DREAM.3D v6.5.171 uses `QuatF`, float matrices, `MatrixMath`, and `acosf`. SIMPLNX promotes the stored float32 quaternions into `QuaternionDType`, calculates orientation matrices and normalized c-axis vectors with double precision, and calls `std::acos`. Both algorithms fold the angle into [0°, 90°] and store float32 results. Neither angle calculation uses the full crystal misorientation search. -which reads the array's pre-write value before adding the new contribution. If `CreateArrayAction` does not zero-initialize the array when `fillValue` is empty (the underlying behavior depends on the `DataStoreUtilities::CreateDataStore` implementation and the IOCollection's default-init contract), the accumulator starts from undefined or implementation-defined state, and the per-feature average is wrong by exactly that initial garbage value. +**Affected users:** Users who compare floating-point angle arrays between DREAM.3D v6.5.171 and SIMPLNX. -**Root cause:** **Bug** (latent) in SIMPLNX. The filter's `preflightImpl` at lines 125-127 of `ComputeFeatureNeighborCAxisMisalignmentsFilter.cpp` constructs the `CreateArrayAction` without a fillValue: +**Recommendation:** Either acceptable within the 0.001° tolerance used by the analytical fixtures for finite pair angles. Compare averages only after accounting for D1, and compare NaN masks separately. This fixture tolerance is not a bound for all orientations. -```cpp -auto createArrayAction = std::make_unique( - DataType::float32, - featurePhases.getIDataStore()->getTupleShape(), - std::vector{1}, - pAvgCAxisMisalignmentsPathValue); -``` - -The algorithm at line 142 assumes the array starts at zero (`getValue(featureIdx) + ...`). If `DataStoreUtilities::CreateDataStore` zero-initializes by default (which it currently does for the in-memory `DataStore` constructor — `m_DataStore = std::vector(numTuples * numComponents);` value-initializes), the bug is dormant. But this behavior is implementation-detail of the underlying DataStore type and is not enforced by the `CreateArrayAction` contract. - -This was not exercised by any V&V fixture because the realistic-microstructure fixture happens to have every feature with `find_avg_misals=true` and a non-zero expected average start with a hex-hex first-neighbor (F1=F2 hex-hex first, F2=F1 hex-hex first, F4=F1 hex-hex first, F5=F2 hex-hex first, F6=F3 NON-hex first but F6's expected avg is 5° from a single hex-hex contribution — so F6 reads its initial value before the first hex-hex write at j=1, exposing the read pattern but the actual default-init behavior in the in-memory build is zero so the test passes). - -**Affected users:** Anyone running this filter on SIMPLNX with `find_avg_misals=true` on a backend where `DataStoreUtilities::CreateDataStore` does not zero-initialize. Currently no shipping backend exhibits non-zero default-init, but future out-of-core DataStore implementations may. Latent → active reclassification recommended once an OOC DataStore lands. - -**Recommendation:** **Defensive fix** — pass `"0"` as the `fillValue` argument to `CreateArrayAction`, OR add an explicit `avgCAxisMisalignmentPtr->fill(0.0f)` at the top of `operator()()` when `FindAvgMisals` is true. Either change is a one-line edit. Confirm by inspection of `DataStoreUtilities::CreateDataStore`'s default-init behavior whether this is currently a real bug or a latent one; the V&V cycle did NOT make this change (out of scope for the divisor-bug-fix focus). +**Evidence:** PR #1472 (`413e6fa46`) changes the SIMPLNX quaternion-to-matrix API and transpose expression. The analytical fixtures use a 0.001° absolute tolerance for finite results. The 2026-10-22 comparison records precision differences in `neutral_hex_pair`, `mixed_average_on`, and `mixed_average_off`, with a maximum pair-angle difference of 0.00005578994751° from DREAM.3D v6.5.171 and exact agreement with DREAM3D-NX with equal NaN positions for a local proof version (`vv_work:ComputeFeatureNeighborCAxisMisalignmentsFilter/comparison/LegacyComparison.md`; proof fixes: `vv_work:ComputeFeatureNeighborCAxisMisalignmentsFilter/notes/proof_commits.md`). --- -## ComputeFeatureNeighborCAxisMisalignmentsFilter-D4 +### ComputeFeatureNeighborCAxisMisalignmentsFilter-D6 -| Field | Value | -|------------------|-------------------------------------------------------------| -| **Deviation ID** | `ComputeFeatureNeighborCAxisMisalignmentsFilter-D4` | -| **Filter UUID** | `636ee030-9f07-4f16-a4f3-592eff8ef1ee` | -| **Status** | active (precision-class; non-deviation in algorithmic sense) | +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeFeatureNeighborCAxisMisalignmentsFilter-D6` | +| **Filter UUID** | `636ee030-9f07-4f16-a4f3-592eff8ef1ee` | +| **Status** | active | -**Symptom:** Per-feature `CAxisMisalignmentList` and `AvgCAxisMisalignments` values differ between SIMPLNX (PR #1472+) and DREAM3D 6.5.171 by approximately `1e-6°` per neighbor pair and up to `~2e-5°` per per-feature average. **Empirically measured 2026-06-04** on the realistic-microstructure A/B fixture: 6.5.171 returns `5.0000` and `15.0000` exactly (legacy float32 path through hand-rolled MatrixMath); SIMPLNX returns values like `5.000001`, `14.999999`, `15.000001` (float32 output cast from Eigen double-precision intermediate). Per-feature averages on F1 (the simplest case): 6.5.171 = `10.0000124`, the local legacy build before the D4 fix = `10.0000124`, SIMPLNX = `10.0000000` (exact). Existing doc note in `docs/ComputeFeatureNeighborCAxisMisalignmentsFilter.md` quotes "~0.0001°" — conservative; actual drift is ~100× smaller. Non-observable on the V&V analytical assertions (the closed-form `|ΔΦ|` derivation matches all three binaries within the `1e-3°` Approx tolerance). +**Symptom:** SIMPLNX computes c-axis angles for pairs with Hexagonal_Low crystal structures. DREAM.3D v6.5.171 writes NaN for those pairs. -**Root cause proven 2026-06-04** by surgically applying the Eigen + double precision conversion to a local build of the legacy source, following the `FindAvgCAxes` and `FindFeatureReferenceCAxisMisorientations` legacy-patch precedents. **Post-fix empirical result: the patched legacy build produces BIT-IDENTICAL output to SIMPLNX on the F#6 fixture** — 18 per-pair `CAxisMisalignmentList` entries + 6 per-feature `AvgCAxisMisalignments` entries all byte-compared and confirmed identical via `h5py` direct comparison. This conclusively attributes the entire pre-fix `~1e-6°` drift to the (Eigen + double) ↔ (hand-rolled MatrixMath + float) precision style difference — no other latent algorithmic difference remains. +**Root cause:** Algorithmic choice. The DREAM.3D v6.5.171 pair gate accepts only equal `Hexagonal_High` values. SIMPLNX accepts equal `Hexagonal_High` or equal `Hexagonal_Low` values. Both compare crystal-structure values, not phase IDs. Different phase IDs with the same accepted crystal structure therefore pass the gate. A Hexagonal_High/Hexagonal_Low pair fails because the two structure values differ. The c-axis calculation uses the orientation matrix and the crystal [001] vector. It does not require a symmetry-operator search. -**Root cause:** **Library swap** during PR #1472 ("EbsdLib bump"). Two pieces of orientation math were replaced: +**Affected users:** Users with Hexagonal_Low neighbor pairs who migrate from DREAM.3D v6.5.171. -1. **Quaternion → orientation matrix conversion.** Legacy and pre-#1472 SIMPLNX used `OrientationTransformation::qu2om(quat)` (a hand-rolled conversion in the OrientationLib/EbsdLib transformation utilities). PR #1472 replaced this with `ebsdlib::QuaternionDType(quat).toOrientationMatrix()` (an EbsdLib member function on the quaternion class itself). Both produce the same matrix up to float64 precision, but the internal arithmetic order differs. +**Recommendation:** Trust SIMPLNX. The same geometric c-axis angle applies to both hexagonal classes. -2. **G-matrix transpose.** Legacy used `OrientationMatrixToGMatrixTranspose(oMatrix)`, which built the transpose into a NEW matrix object. PR #1472 replaced this with `oMatrix.transpose()` (Eigen's lazy `Transpose` view). The downstream `.transpose() * cAxis` operation then evaluates the transpose-multiply as a single Eigen expression. Numerically identical to a pre-built transposed matrix multiplied by the same vector, but the rounding sequence is different. - -**Affected users:** Anyone diff-ing per-pair c-axis misalignment values between DREAM3D 6.5.171 output and post-PR-#1472 SIMPLNX output. The shift is well below typical EBSD measurement resolution (~`0.5°`) and will not materially affect downstream microstructural analyses. - -**Recommendation:** **Trust SIMPLNX.** The shift is precision-class noise from a library swap, not an algorithmic difference. The Eigen-based form is the cleaner expression and is consistent with the rest of the post-#1472 OrientationAnalysis plugin. The doc note's "0.0001 degrees" estimate is in line with what the V&V cycle observed — no growth over the original PR #1472 commit. +**Evidence:** The source for each DREAM3D-NX release from v7.0.0 through v7.4.2 includes Hexagonal_Low in the pair gate. No current analytical unit-test fixture directly selects Hexagonal_Low. The 2026-10-22 `hex_low_fold` case records two list entries and two averages as NaN in DREAM.3D v6.5.171 and as 59.999996185302734° in both DREAM3D-NX and a local proof version, whose outputs match exactly with equal NaN positions (`vv_work:ComputeFeatureNeighborCAxisMisalignmentsFilter/comparison/LegacyComparison.md`; proof fixes: `vv_work:ComputeFeatureNeighborCAxisMisalignmentsFilter/notes/proof_commits.md`). --- -## ComputeFeatureNeighborCAxisMisalignmentsFilter-D5 +## Part 2 — SIMPLNX defects and behavior changes -| Field | Value | -|------------------|--------------------------------------------------------------------| -| **Deviation ID** | `ComputeFeatureNeighborCAxisMisalignmentsFilter-D5` | -| **Filter UUID** | `636ee030-9f07-4f16-a4f3-592eff8ef1ee` | -| **Status** | active (UX-only downgrade; preflight banner no longer shown in GUI parameter panel — execute-time algorithm warning still surfaces correctly to all users) | +### ComputeFeatureNeighborCAxisMisalignmentsFilter-D2 -**Symptom (partially retracted after empirical A/B 2026-06-04):** PR #1438 moved a FILTER-LEVEL preflight banner from `resultOutputActions.warnings()` to `preflightUpdatedValues`. The ALGORITHM-side warning at lines 53-56 of `Algorithms/ComputeFeatureNeighborCAxisMisalignments.cpp` (`"Non Hexagonal phases were found. All calculations for non Hexagonal phases will be skipped and a NaN value inserted."`) is STILL pushed into the algorithm's `Result<>::warnings()` collection and DOES surface in CLI nxrunner output. Empirically confirmed during the A/B test: SIMPLNX nxrunner stderr printed `Code: -1563 Message: Non Hexagonal phases were found...` when run on the realistic-microstructure fixture. +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeFeatureNeighborCAxisMisalignmentsFilter-D2` | +| **Filter UUID** | `636ee030-9f07-4f16-a4f3-592eff8ef1ee` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.2 use the output array as the accumulator. DREAM.3D v6.5.171 was not affected by the initialization dependency; it supplies an explicit zero fill value. | +| **Fixed in** | The DREAM3D-NX release after v7.4.2 | -What was actually lost in PR #1438 is the **preflight-time** banner that GUI users see in the parameter panel before they hit "Execute." Pipeline-mode users still see the same warning, but only at execute-time (when the algorithm hits the early-exit / warning branch). This is a UX downgrade (delayed feedback) but NOT the "users see nothing" regression originally claimed. +**Symptom:** Current SIMPLNX initializes average accumulators to zero without reading the output array's initial values. -**Root cause:** **Intentional UX change** in PR #1438 ("Microtexture cleanup"). The PR author's intent was likely to reduce CLI noise by demoting an "informational" message — but the message describes a real algorithmic behavior (NaN insertion) that downstream consumers need to know about, especially when running the shipping `EBSD_Hexagonal_Data_Analysis.d3dpipeline` on data that turns out to contain non-hex phases. +**Root cause:** Behavior change. DREAM3D-NX v7.0.0 through v7.4.2 add angles directly to the output array created without an explicit fill value. Current SIMPLNX initializes a buffer with `avgCAxisBuf.resize(totalFeatures, 0.0f)`, accumulates in that buffer, and writes it once at the end. This removes the dependency on the output store's initial contents. This entry records the initialization change. -The current algorithm code still produces the warning correctly (lines 53-56), but it's pushed into the algorithm's `Result<>` return, which is surfaced via different channels in GUI vs CLI mode. PR #1438's specific change was the demotion at the FILTER level, not the algorithm level. +**Affected users:** Users who enable the average output. The accumulation now starts at an explicit zero for every feature. -**Affected users:** Pipeline-mode users (CLI / Python / nxrunner) running this filter on mixed-phase data. They will see NaN values appear in the `CAxisMisalignmentList` output without any warning that explains why. GUI users still see the message correctly. +**Recommendation:** Trust SIMPLNX. Explicit zero initialization makes the average independent of the output store's initial contents. -**Recommendation:** **Restore the warning to `Result<>::warnings()`** so pipeline-mode users see it. This is a one-line addition to the filter or algorithm — pushing the warning into both `Result<>` AND `preflightUpdatedValues` is fine. The V&V cycle did NOT make this change (out of scope for the divisor-bug-fix focus); flag as follow-up. +**Evidence:** PR #1568 (`4ec21e93f`) adds the zero-initialized buffer. The Class 1 averages and Class 4 mean invariant define the expected numerical result. --- -## ComputeFeatureNeighborCAxisMisalignmentsFilter-D6 - -| Field | Value | -|------------------|--------------------------------------------------------------------| -| **Deviation ID** | `ComputeFeatureNeighborCAxisMisalignmentsFilter-D6` | -| **Filter UUID** | `636ee030-9f07-4f16-a4f3-592eff8ef1ee` | -| **Status** | active (behavior class; SIMPLNX correct since port, legacy gap) | +### ComputeFeatureNeighborCAxisMisalignmentsFilter-D5 -**Symptom:** For datasets containing features whose shared phase has Laue class **Hexagonal_Low** (6/m), SIMPLNX computes the c-axis misalignment exactly as for Hexagonal_High (6/mmm); DREAM3D 6.5.171 writes `NaN` because the legacy phase-match gate restricts the calculation to Hex_High pairs only. +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeFeatureNeighborCAxisMisalignmentsFilter-D5` | +| **Filter UUID** | `636ee030-9f07-4f16-a4f3-592eff8ef1ee` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.4.0 through v7.4.2 contain the preflight information-channel change. DREAM.3D v6.5.171 was not affected; it has no corresponding SIMPLNX preflight channel. | +| **Fixed in** | DREAM3D-NX v7.4.0 | -**Root cause:** **Library + algorithmic choice** — same root pattern as D1 of `ComputeFeatureFaceMisorientations`. +**Symptom:** The general hexagonal-symmetry notice is a preflight updated value instead of a preflight warning. Mixed-structure execution still returns warning `-1563`. -The legacy code at `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/FindFeatureNeighborCAxisMisalignments.cpp:286` reads: -```cpp -if(phase1 == phase2 && (phase1 == Ebsd::CrystalStructure::Hexagonal_High)) -``` -which restricts the c-axis-misalignment computation to Hex_High↔Hex_High neighbor pairs only. +**Root cause:** Behavior change. DREAM3D-NX v7.4.0 moves the general notice from `resultOutputActions.warnings().push_back(...)` to `preflightUpdatedValues.push_back(...)` in the filter. The algorithm's mixed-structure warning remains. Current execution returns `-1562` if no hexagonal ensemble exists and warning `-1563` if hexagonal and non-hexagonal ensembles coexist. -The SIMPLNX code at `Algorithms/ComputeFeatureNeighborCAxisMisalignments.cpp:114` reads: -```cpp -if(xtalPhase1 == xtalPhase2 && (xtalPhase1 == ebsdlib::CrystalStructure::Hexagonal_High || xtalPhase1 == ebsdlib::CrystalStructure::Hexagonal_Low)) -``` -which correctly handles BOTH hex Laue classes. The c-axis math is independent of symmetry-operator-set choice (the algorithm only uses the orientation matrix and the c-axis direction; no `LaueOps` calls are made), so the same code path produces the mathematically correct misalignment for both hex Laue classes. The legacy restriction to Hex_High was historical — the OrientationLib of that era only had `LaueOps` symmetry operators for Hex_High, and the original author conservatively restricted the gate to match what other hex-aware filters could handle. This filter doesn't need symmetry operators, so the restriction is unnecessary. +**Affected users:** Clients that read preflight warnings but do not read preflight updated values. The filter's result channel still contains the execution-time warning. -The early-exit preflight in SIMPLNX (lines 35-45) also treats both Hex Laue classes as "valid hex phases" for the all-non-hex error and mixed-phase warning logic. Legacy 6.5.171 only counts Hex_High features. +**Recommendation:** Read preflight updated values for the general notice and execution warnings for the data-dependent result. No warning-restoration fix is required by this entry. -**Not observable on the F#6 V&V fixture** because the realistic-microstructure fixture only uses Hex_High features (no Hex_Low). The deviation IS observable on any real EBSD dataset containing Hex_Low phases (e.g., wurtzite-structure materials, some intermetallics). - -**Affected users:** Anyone running this filter on DREAM3D 6.5.171 with a dataset containing Hex_Low phases. Legacy writes `NaN` for those features' misalignment entries; SIMPLNX computes the real c-axis misalignment. - -**Recommendation:** **Trust SIMPLNX.** The c-axis math is correct for both hex Laue classes; the legacy restriction was overly conservative. **Proven 2026-06-04** by applying the fix to a local build of the legacy source, bundled with D4 (Eigen+double conversion). Post-fix: the patched legacy `FindFeatureNeighborCAxisMisalignments` accepts both Hex_High and Hex_Low pairs, identical to SIMPLNX. +**Evidence:** PR #1438 (`e6896714b`) introduces the channel change. The release sources return the general notice as a preflight warning through v7.2.2 and as an updated value in v7.4.0 through v7.4.2. --- -## Non-deviations (algorithm characteristics common to both filters) - -The following behaviors are NOT deviations — SIMPLNX (post-D1 fix) and DREAM3D 6.5.171 (with D1 still present) agree on them where D1 is not exercised. Captured here so future engineers don't re-discover them and propose them as deviations. - -### NaN entry on phase mismatch and non-hex Laue class - -Both implementations write `NaN` (via `std::nanf("")` or the C macro `NAN`) into the per-neighbor `CAxisMisalignmentList` entry when the focal feature's phase differs from the neighbor's phase, or when the shared phase's Laue class is not `Hexagonal_High` (or `Hexagonal_Low` — both are accepted). **Both filters share this behavior** — algorithm characteristic, not a defect. - -### Per-feature outer-loop iteration starts at index 1 (skips background feature 0) +### ComputeFeatureNeighborCAxisMisalignmentsFilter-D7 -Both implementations iterate `for(size_t i = 1; i < totalFeatures; i++)` in the per-feature outer loop, skipping the background feature at index 0. The `CAxisMisalignmentList[0]` and `AvgCAxisMisalignments[0]` entries are therefore left at their initialized default values (empty list and `0.0f`, respectively). **Both filters share this behavior**. +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeFeatureNeighborCAxisMisalignmentsFilter-D7` | +| **Filter UUID** | `636ee030-9f07-4f16-a4f3-592eff8ef1ee` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was affected. | +| **Fixed in** | The DREAM3D-NX release after v7.4.2 | -### Default output array name rename (formerly proposed as D3) +**Symptom:** A negative or excessive focal or neighbor phase index can cause an out-of-bounds read from Crystal Structures. -PR #1438 renamed the default output array from `"AvgCAxisMisalignments"` to `"AvgNeighborCAxisMisalignments"`, and reworded the parameter labels: -- `"C-Axis Misalignment List"` → `"Feature C-Axis Misalignment NeighborList"` -- `"Average C-Axis Misalignments"` → `"Feature Average C-Axis Misalignments"` +**Root cause:** Bug. DREAM3D-NX v7.0.0 through v7.4.2 use `crystalStructures[featurePhases[...]]` without checking the phase range. DREAM.3D v6.5.171 uses the same unchecked lookup. Current SIMPLNX checks both focal and neighbor phase indices before reading the cached crystal structures. The check returns `-1564` with the array paths, index, invalid value, and valid range. -This is **user-facing migration noise**, not a behavioral deviation. The SIMPLNX backwards-compat path (PR #1588) preserves the SIMPL conversion semantics. Existing pipeline files in the repo were re-saved with the new name; user-saved pipelines that explicitly named arrays still work. Documented here for completeness; not a numbered deviation. +**Affected users:** Users whose processed features have phase values outside the Crystal Structures tuple range. An unused background phase is not read because the feature loop starts at one. -### EbsdLib 2.4.1 CubicOps precision improvement does NOT apply +**Recommendation:** Trust SIMPLNX. Correct the invalid phase or provide its ensemble tuple. -Unlike F#1 / F#2 / F#4 / F#5, this filter does NOT route through `LaueOps::calculateMisorientation` or `CubicOps::calculateMisorientationInternal`. The algorithm computes c-axis vectors directly from the orientation matrix (`R^T · [0,0,1]`) and takes `arccos(c1·c2)` without any cubic-symmetry-operator search. The EbsdLib 2.4.1 precision improvement therefore has no effect on this filter's output, and there is no `D-precision` entry analogous to the other filters in the cycle. +**Evidence:** PR #1568 (`4ec21e93f`) adds the bounds checks. `Phase Index Bounds` checks -1 and 2 against a two-tuple ensemble array for both lookup locations and permits an invalid unused background phase. diff --git a/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFeatureNeighborMisorientationsFilter.md b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFeatureNeighborMisorientationsFilter.md index 3e69d220bd..c181011e03 100644 --- a/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFeatureNeighborMisorientationsFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFeatureNeighborMisorientationsFilter.md @@ -1,82 +1,69 @@ -# Deviations from DREAM3D 6.5.171: ComputeFeatureNeighborMisorientationsFilter +# Deviations from DREAM.3D v6.5.171: ComputeFeatureNeighborMisorientationsFilter -This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent (`FindMisorientations`, source at `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/FindMisorientations.{h,cpp}` in DREAM3D 6.5.171). - -Entries are referenced by stable ID (`ComputeFeatureNeighborMisorientationsFilter-D`) from the V&V report and from public migration guidance. The ID is stable across renames; the Filter UUID field is the permanent cross-reference anchor. - -## Comparison summary - -The legacy A/B comparison was performed by **source inspection** rather than empirical run. Justification: SIMPLNX `ComputeFeatureNeighborMisorientations::operator()()` is a clean Port of legacy `FindMisorientations::execute()` (same per-feature outer loop, same per-neighbor inner loop, same phase-match gate, same optional per-feature averaging finalize). Both implementations share a divisor bug at the `tempMisoList` reassignment (D1 below) — verified by grep of the legacy source. The bug went undetected for the lifetime of both implementations because the `ComputeAvgMisors=true` test in the SIMPLNX suite was an `[.][UNIMPLEMENTED][!mayfail]` stub with zero CI coverage. +Part 1 records the divisor and precision differences from DREAM.3D v6.5.171 (D1 and D2). Part 2 records the phase-index defect fixed in SIMPLNX (D3). Both parts use one stable ID sequence. The Filter UUID is the permanent cross-reference anchor. --- -## ComputeFeatureNeighborMisorientationsFilter-D1 - -| Field | Value | -|------------------|--------------------------------------------------------------------------------------------------------------------------------------| -| **Deviation ID** | `ComputeFeatureNeighborMisorientationsFilter-D1` | -| **Filter UUID** | `0b68fe25-b5ef-4805-ae32-20acb8d4e823` | -| **Status** | active (SIMPLNX fixed 2026-06-02; legacy 6.5.171 still has the bug); SIMPLNX-side fix ships in DREAM3D-NX **7.4.2** | +## Part 1 — Differences from DREAM.3D v6.5.171 -**Symptom:** Per-feature `AvgMisorientations` (output of `ComputeAvgMisors=true` / legacy `FindAvgMisors=true`) differ between SIMPLNX (post-2026-06-02 fix) and DREAM3D 6.5.171 on any dataset where features have mixed-phase neighbor lists. The legacy result depends on the *order* in which neighbors appear in the per-feature `NeighborList`: if the last-iterated neighbor is a phase match, the divisor used is the full neighbor-list length (incorrect); if the last neighbor is a phase mismatch, the divisor is decremented by 1 from the full length (the per-mismatch decrement at line 90 happens to be the last write to `tempMisoList`). The legacy result is therefore correct in some cases by accident and wrong by up to `(N-K) / N` of the true value in others, where N is the neighbor count and K is the number of phase-matched neighbors. +### ComputeFeatureNeighborMisorientationsFilter-D1 -The bug is **non-observable on the V&V data fixtures' single-phase configurations** (no phase mismatches → no decrements → divisor matches list length, which equals the number of matches, which is correct). The bug **IS observable** on the bug-exposing fixture `Mixed Phase Neighbors (exposes divisor bug)`, which constructs a neighbor list `[match, mismatch, match]` for which the expected average is `(5 + 10) / 2 = 7.5°` but the buggy code produces `(5 + 10) / 3 = 5.0°`. +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeFeatureNeighborMisorientationsFilter-D1` | +| **Filter UUID** | `0b68fe25-b5ef-4805-ae32-20acb8d4e823` | +| **Status** | active | -**Root cause:** **Bug** in both legacy DREAM3D 6.5.171 and SIMPLNX pre-fix. +**Symptom:** With averaging enabled, excluded neighbors can lower the average and make it depend on neighbor order. -The legacy code at `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/FindMisorientations.cpp` (lines TBD) and the SIMPLNX pre-fix code at `Algorithms/ComputeFeatureNeighborMisorientations.cpp:75` both contain `tempMisoList = featureNeighborList.size();` *inside* the inner per-neighbor j-loop. The intended behavior is for `tempMisoList` to start each outer-loop iteration (per feature) at `featureNeighborList.size()` and then decrement by 1 for each phase-mismatched neighbor (line 90: `tempMisoList > 0 ? tempMisoList-- : tempMisoList = 0;`). Because the reassignment happens at the *top* of each j-iteration, the decrement from the *previous* iteration is clobbered. The result is that only the *last* j-iteration's match/mismatch state actually affects `tempMisoList`: if the last neighbor is a match, the assignment runs and the decrement doesn't, so the final divisor is N; if the last neighbor is a mismatch, both the assignment and the decrement run, so the final divisor is N - 1. +**Root cause:** bug. DREAM.3D v6.5.171 `FindMisorientations::execute()` resets `tempMisoList` to the list length inside each neighbor iteration. The next iteration therefore discards the preceding decrement. For a nonempty list of length N, the final divisor is N when the last neighbor is compatible, or N−1 when it is incompatible. SIMPLNX inherited this code. DREAM3D-NX v7.4.2 initializes the divisor once per feature, before the neighbor loop, and preserves all decrements. Compatibility means equal, valid crystal-structure indices. It does not require equal phase IDs. A list with one incompatible neighbor at the end can produce the correct divisor by coincidence. Multiple incompatible neighbors can still produce a wrong divisor when the last neighbor is incompatible. An all-incompatible list with more than one neighbor can produce zero instead of NaN. -The SIMPLNX fix (2026-06-02) moves the `tempMisoList = featureNeighborList.size();` assignment from line 75 to before the inner j-loop (alongside `tempMisorientationLists[i].assign(...)` at line ~67), so the assignment runs once per outer-loop iteration (per feature) and the decrement is preserved across j-iterations. The result is the mathematically correct divisor: the number of phase-matched neighbors. +**Affected users:** Users of DREAM.3D v6.5.171 or DREAM3D-NX v7.0.0 through v7.4.1 who enable average misorientation on features with incompatible neighbors. Single-phase data with valid crystal structures does not trigger this divisor defect. -The bug went undetected for the lifetime of both implementations because: -1. **The legacy 6.5.171 implementation had no automated test coverage of the `ComputeAvgMisors=true` path** (legacy DREAM3D's CI tested filters with default parameter values; this parameter defaults to false in many user-facing pipelines and the test infrastructure didn't sweep over both values). -2. **The SIMPLNX Port preserved the bug** without a regression test that exercises mixed-phase neighbor lists. The `ComputeAvgMisors=true` test in `ComputeFeatureNeighborMisorientationsTest.cpp` was an `[.][UNIMPLEMENTED][!mayfail]` stub with the comment "TODO: needs to be implemented. This will need the input .dream3d file to be regenerated with the missing data generated using DREAM3D 6.6". Zero CI coverage. -3. **The retroactive bug-triage cycle (2026-05) caught it** by source inspection. Documented in `/Users/mjackson/Desktop/bug_triage.md` as Bug #2. +**Recommendation:** trust SIMPLNX. The corrected average divides the sum by the number of compatible neighbors. -**Affected users:** Anyone running DREAM3D 6.5.171 or SIMPLNX pre-2026-06-02 with `ComputeAvgMisors=true` (legacy `FindAvgMisors=true`) on data containing features with mixed-phase neighbor lists. In practice this affects any multi-phase EBSD dataset that has at least one feature whose neighbor list includes both same-phase and different-phase neighbors. Single-phase datasets are unaffected. Datasets where the bug "accidentally" produces the correct divisor (every feature's neighbor list ends in a mismatch) are also unaffected. - -**Recommendation:** **Trust SIMPLNX (post-2026-06-02 fix).** The pre-fix per-feature `AvgMisorientations` values from both DREAM3D 6.5.171 and pre-fix SIMPLNX are mathematically incorrect for any feature with a mixed-phase neighbor list. Users migrating from 6.5.171 should expect per-feature average misorientations to shift toward the mathematically correct value, with the shift size proportional to the fraction of phase-mismatched neighbors per feature. - -A legacy backport branch of `FindMisorientations.cpp` with the same fix (move the `tempMisoList` reassignment outside the inner loop) would produce the corrected values on DREAM3D 6.5.171 for users requiring legacy-version-parity post-correction. The fix is mechanically the same as the SIMPLNX fix and is a one-line move. No such backport branch is currently maintained. +**Evidence:** PR #1631 (`ec22ac375`) contains the divisor fix. Test sensitivity verification: `Class 1 - Mixed Phase Neighbors (exposes divisor bug)` uses neighbors `[2, 4, 3]` and expects `[5°, NaN, 10°]` with mean 7.5°. Resetting the divisor inside the loop instead gives 5°. `Class 1 - Mismatch Last Order` uses `[2, 3, 4]` as the control. `Class 4 - Invariants` compares the mean with the sum and count of finite list entries. All pass in CI. The run against DREAM.3D v6.5.171 on 2026-09-17 gives 4.9999804° for the middle-mismatch mean and 7.5° in SIMPLNX. In the mixed-phase Small IN100 case, 508 legacy feature averages differ from the correct formula applied to the legacy lists. SIMPLNX and a local proof version satisfy that formula for every feature. See `vv_work:ComputeFeatureNeighborMisorientationsFilter/comparison/case2_mixed_phase_mismatch_middle.txt`, `vv_work:ComputeFeatureNeighborMisorientationsFilter/comparison/case5_small_in100_mixed_phase.txt`, and the provenance sidecar for source changes after the comparison. Every release from `upstream/release/v7.0.0` through `upstream/release/v7.4.1` resets the divisor inside the neighbor loop. The `upstream/release/v7.4.2` source initializes it before that loop. The algorithm path is `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeMisorientations.cpp` in v7.0.0 through v7.0.3 and `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeFeatureNeighborMisorientations.cpp` in later releases. The DREAM.3D `v6.5.171` tag contains the defect in `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/FindMisorientations.cpp`. --- -## ComputeFeatureNeighborMisorientationsFilter-D2 - -| Field | Value | -|------------------|-------------------------------------------------------------| -| **Deviation ID** | `ComputeFeatureNeighborMisorientationsFilter-D2` | -| **Filter UUID** | `0b68fe25-b5ef-4805-ae32-20acb8d4e823` | -| **Status** | active (precision-class; non-deviation in algorithmic sense) | - -**Symptom:** Per-neighbor `MisorientationList` values and per-feature `AvgMisorientations` values differ between SIMPLNX (EbsdLib 2.4.1+, post-D1 fix) and DREAM3D 6.5.171 on real EBSD datasets containing cubic-phase features with grain-pair boundaries near cubic symmetry operators (e.g., 4-fold about c-axis, 3-fold about [111], 2-fold about face-diagonal). Per-neighbor values shift by sub-`0.0001°` (within float precision), but the magnitude amplifies when averaged across many neighbors at the per-feature level. On the V&V data fixtures (pure φ1 rotations about z, no sym-op-aligned neighbor pairs), no observable deviation. +### ComputeFeatureNeighborMisorientationsFilter-D2 -**Root cause:** **Precision** — not an algorithm change in either implementation. +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeFeatureNeighborMisorientationsFilter-D2` | +| **Filter UUID** | `0b68fe25-b5ef-4805-ae32-20acb8d4e823` | +| **Status** | active | -The deviation traces to the EbsdLib 2.4.1 release commit `5c8c993` (BlueQuartz Software, 2026-05-29), which replaces a precision-fragile `acos(w)` form in `CubicOps::calculateMisorientationInternal` with a numerically-stable `2·atan2(|v|, w)` form using the explicit reduced-quaternion `v` components. The precision improvement is real and mathematically more correct; it manifests for cubic misorientations whose minimum-rotation-axis representation lies on or near a cubic symmetry operator. +**Symptom:** Neighbor angles and feature averages can differ slightly from DREAM.3D v6.5.171 even when the averaging divisor is correct. -This filter is a clean Port of `FindMisorientations` (modulo the D1 divisor bug, which existed in both implementations and is now fixed in SIMPLNX). The SIMPLNX algorithm reproduces the legacy per-feature outer loop, per-neighbor inner loop, and per-feature average finalization. The legacy filter consumes `OrientationLib::CubicOps::getMisoQuat` (pre-fix `acos`-form, float32); the SIMPLNX filter consumes `ebsdlib::CubicOps::calculateMisorientation` (post-fix `2·atan2`-form, QuatD). The difference is entirely in the EbsdLib precision improvement, NOT in this filter. +**Root cause:** precision. DREAM.3D v6.5.171 uses `QuatF`, `getMisoQuat()`, and a float angle. SIMPLNX promotes stored float32 quaternions to `QuatD`, calls `calculateMisorientation()`, and converts the double angle to degrees before float32 storage. EbsdLib also replaces the cubic `acos` angle calculation with `2*atan2(norm(v), w)` using explicit reduced-quaternion vector components. These differences affect individual angles and the means formed from them. -For the full root-cause walkthrough of the EbsdLib precision improvement, see the precedent characterization in `vv/deviations/BadDataNeighborOrientationCheckFilter.md` §"Non-deviations" → "EbsdLib 2.4.1 CubicOps precision improvement". The characterization there applies equally to this filter. As with `ComputeFeatureReferenceMisorientationsFilter-D1`, this filter's per-feature averaging amplifies the per-voxel precision shift across the feature's neighbor list. +**Affected users:** Users who compare floating-point neighbor angles or feature means with DREAM.3D v6.5.171. The archived measurements include cubic analytical inputs and cubic and mixed cubic/hexagonal Small IN100 inputs. -**Affected users:** Anyone migrating from DREAM3D 6.5.171 to SIMPLNX on cubic-phase EBSD data with features whose neighbor lists include sym-op-aligned grain-pair boundaries. Non-cubic data and data without sym-op-aligned boundaries are unaffected. +**Recommendation:** trust SIMPLNX. The analytical tests use a `1e-3°` tolerance and independent expected values. The precision difference does not change the definition of the average. -**Recommendation:** **Trust SIMPLNX.** The 6.5.171 result was limited by float32-input ULP noise amplified by `acos`-near-1 catastrophic cancellation; SIMPLNX returns the mathematically correct value. The shift is well below typical EBSD measurement resolution and will not materially affect downstream microstructural analyses. +**Evidence:** EbsdLib commit `5c8c993` contains the cubic angle-calculation change. On 2026-09-17, the three analytical cases differ from DREAM.3D v6.5.171 by up to `3.05176e-5°` per neighbor. Cubic Small IN100 differs by up to `0.000337124°` per neighbor and `7.24792e-5°` per feature mean. The local proof version matches all five cases within `rtol=1e-6, atol=1e-6`, with paired NaNs; the production cases retain rounding differences. See `vv_work:ComputeFeatureNeighborMisorientationsFilter/comparison/summary.md` and `vv_work:ComputeFeatureNeighborMisorientationsFilter/notes/residual_rounding_analysis.md`. --- -## Non-deviations (algorithm characteristics common to both filters) +## Part 2 — SIMPLNX defects and behavior changes -The following behaviors are NOT deviations — SIMPLNX (post-D1 fix) and DREAM3D 6.5.171 (with D1 still present) agree on them where the D1 bug is not exercised. Captured here so future engineers don't re-discover them and propose them as deviations. +### ComputeFeatureNeighborMisorientationsFilter-D3 -### NaN entry on phase mismatch +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeFeatureNeighborMisorientationsFilter-D3` | +| **Filter UUID** | `0b68fe25-b5ef-4805-ae32-20acb8d4e823` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was affected. | +| **Fixed in** | The DREAM3D-NX release after v7.4.2 | -Both implementations write `NaN` (via `std::numeric_limits::quiet_NaN()` or ``'s `NAN`) into the per-neighbor `MisorientationList` entry when the neighbor's phase differs from the focal feature's phase, or when the focal feature's `laueClass1` is out of range for `orientationOps`. **Both filters share this behavior** — algorithm characteristic, not a defect. +**Symptom:** A negative or out-of-range feature phase can cause an out-of-bounds read from Crystal Structures. -### Per-feature outer-loop iteration starts at index 1 (skips background feature 0) +**Root cause:** bug. Both the current-feature and neighbor-feature lookups used phase values directly as ensemble indices. Preflight validates array shapes and tuple counts, not the phase values. The filter now checks phase bounds before both lookups in `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeFeatureNeighborMisorientations.cpp`. Invalid phases now return error `-34502`. Background feature zero is skipped by the outer loop. -Both implementations iterate `for(size_t i = 1; i < totalFeatures; i++)` in the per-feature outer loop, skipping the background feature at index 0. The `MisorientationList[0]` and `AvgMisorientations[0]` entries are therefore left at their initialized default values (empty list and `0.0f`, respectively). **Both filters share this behavior**. +**Affected users:** Users whose participating features contain phase indices outside `[0, number of Crystal Structures tuples)`. -### Self-misorientation not computed +**Recommendation:** trust SIMPLNX. Correct the reported phase value or supply the corresponding Crystal Structures entry before execution. -Neither implementation includes the focal feature `i` in its own neighbor list (the `NeighborList` input is assumed to be a list of *other* features that share at least one boundary with feature `i`, not including `i` itself). Both implementations therefore do not compute `misorientation(i, i)` (which would be `0°` by definition). **Both filters share this assumption** — it is a property of how `ComputeFeatureNeighbors` (the upstream filter that produces the `NeighborList`) is conventionally used in the SIMPLNX and DREAM3D 6.5.171 pipelines. +**Evidence:** PR #1568 (`4ec21e93f`) contains the phase-bounds fix. `Phase Index Bounds` supplies -1 and 2 with a two-tuple ensemble array. It checks both current and neighbor errors and successful execution when only the background phase is invalid. All pass in CI. Every release source from `upstream/release/v7.0.0` through `upstream/release/v7.4.2` has both unchecked lookups, using the algorithm paths listed under D1. DREAM.3D tag `v6.5.171`, `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/FindMisorientations.cpp`, also has both unchecked lookups. diff --git a/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFeatureReferenceCAxisMisorientationsFilter.md b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFeatureReferenceCAxisMisorientationsFilter.md index eaa5208ee2..007f2a01d8 100644 --- a/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFeatureReferenceCAxisMisorientationsFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFeatureReferenceCAxisMisorientationsFilter.md @@ -1,128 +1,97 @@ -# Deviations from DREAM3D 6.5.171: ComputeFeatureReferenceCAxisMisorientationsFilter +# Deviations from DREAM.3D v6.5.171: ComputeFeatureReferenceCAxisMisorientationsFilter -This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent (`FindFeatureReferenceCAxisMisorientations`, source at `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/FindFeatureReferenceCAxisMisorientations.{h,cpp}` in DREAM3D 6.5.171). +Part 1 lists differences from DREAM.3D v6.5.171 (D1 and D2). Part 2 lists fixed SIMPLNX defects (D3 and D4). Both parts use the stable `ComputeFeatureReferenceCAxisMisorientationsFilter-D` sequence. The Filter UUID is the permanent cross-reference anchor. -Entries are referenced by stable ID (`ComputeFeatureReferenceCAxisMisorientationsFilter-D`) from the V&V report and from public migration guidance. The ID is stable across renames; the Filter UUID field is the permanent cross-reference anchor. - -## Comparison summary - -The legacy A/B comparison was performed **empirically** on 2026-06-10 against three binaries: - -- **A:** DREAM3D 6.5.171 (official release, `/Users/mjackson/Applications/DREAM3D.app/Contents/bin/PipelineRunner`). -- **B:** A local build of the DREAM3D 6.5.171 source with surgical fixes applied — used only to prove the root cause of each deviation. -- **C:** SIMPLNX. +--- -All three binaries were run on the same hand-built `.dream3d` input file containing the realistic-microstructure fixture (5×5×1 ImageGeom, 5 features incl. one all-cubic F3, pure-Φ Bunge ZXZ rotations matching the SIMPLNX `AnalyticalFixtures::BuildRealisticMicrostructure()` helper). A/B test workspace and artifacts at `/Users/mjackson/Desktop/FRCAM_AB_Test/`. +## Part 1 — Differences from DREAM.3D v6.5.171 -**Result summary:** +### ComputeFeatureReferenceCAxisMisorientationsFilter-D1 -- **Patched legacy build (B) ≡ SIMPLNX byte-for-byte** across `FeatureReferenceCAxisMisorientations` (25 cells), `FeatureAvgCAxisMisorientations` (6 features), and `FeatureStdevCAxisMisorientations` (6 features). The two pre-existing surgical fixes to the legacy source (`ENH: FindFeatureReferenceCAxisMisorientations - Update to use Eigen` + `BUG: ... StdDev double precision`) were sufficient. -- **6.5.171 produces materially-different output** in two distinct ways — D1 (non-hex feature handling) and D4 (EbsdLib precision) — both documented below. +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeFeatureReferenceCAxisMisorientationsFilter-D1` | +| **Filter UUID** | `16c487d2-8f99-4fb5-a4df-d3f70a8e6b25` | +| **Status** | active | -This filter is the c-axis analog of `ComputeFeatureReferenceMisorientationsFilter`. Important distinction: this filter does NOT route through `LaueOps::calculateMisorientation` — it uses Eigen for the c-axis vector math (orientation matrix → c-axis rotation → `arccos(c1 · avgCAxis)` folded to `[0°, 90°]`). The EbsdLib 2.4.1+ `calculateMisorientation` precision improvement that surfaced in F#1/F#2/F#4/F#5 of this V&V cycle does **not** apply here. The precision drift documented under D4 below has a different mechanism: hand-rolled `MatrixMath` + float32 stddev accumulation on the legacy side vs Eigen + double accumulation on the SIMPLNX side. +**Symptom:** DREAM.3D v6.5.171 includes non-hexagonal cells in c-axis statistics; SIMPLNX skips them and gives NaN feature statistics when no hexagonal cell contributes. ---- +**Root cause:** Bug. DREAM.3D v6.5.171 gates the angle calculation only on positive FeatureIds and CellPhases. It has no Crystal Structures input or hexagonal-phase check. A cubic cell therefore produces an angle from its quaternion and the supplied feature reference vector. That angle has no meaning under this filter's hexagonal-phase contract. -## ComputeFeatureReferenceCAxisMisorientationsFilter-D1 +DREAM3D-NX v7.0.0 through v7.4.2 check Hexagonal-High or Hexagonal-Low before accumulating. DREAM.3D v6.5.171 was affected by the non-hexagonal cell defect; these DREAM3D-NX releases were not affected. DREAM3D-NX v7.0.0 through v7.2.2 write zero mean and standard deviation when no cells contribute. DREAM3D-NX v7.4.0 and v7.4.1 obtain NaN through division by zero. DREAM3D-NX v7.4.2 writes quiet NaN explicitly. -| Field | Value | -|------------------|--------------------------------------------------------------------------------------------------------------------------------------| -| **Deviation ID** | `ComputeFeatureReferenceCAxisMisorientationsFilter-D1` | -| **Filter UUID** | `16c487d2-8f99-4fb5-a4df-d3f70a8e6b25` | -| **Status** | active (SIMPLNX fixed pre-V&V via PR #1438; legacy 6.5.171 still has the bug — root cause proven by applying the same fix to a local build of the legacy source) | +DREAM.3D v6.5.171 also replaces a zero-count feature mean with zero. Its standard-deviation calculation has no zero-count guard. Current SIMPLNX explicitly writes NaN for both outputs. It rejects an ensemble with no hexagonal phase with `-9802` and reports mixed phases with `-9803`. -**Symptom:** Per-cell and per-feature outputs for non-hex features differ between SIMPLNX and DREAM3D 6.5.171. Legacy 6.5.171s c-axis projection treats every cubic cell's quaternion as if it were hex. For a feature whose cells are *all* non-hex, the legacy filter produces `featAvg = 0.0 or whatever-the-projection-yields` instead of the analytically-correct `NaN`. The issue exists in the per-feature population stddev calculation as well. +**Affected users:** Users who process non-hexagonal cells, mixed-phase data, or features with no contributing cells. -**Root cause:** **Bug** in legacy DREAM3D 6.5.171. +**Recommendation:** Trust SIMPLNX. Zero cell angles mark skipped cells, and NaN feature statistics mark the absence of valid contributors. -The legacy code at `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/FindFeatureReferenceCAxisMisorientations.cpp:281` gates the per-cell math on `if(m_FeatureIds[point] > 0 && m_CellPhases[point] > 0)` — no crystal-structure check. Cubic cells fall through to lines 290-308 which compute `c = R^T · [0,0,1]` (the cell-quat-derived c-axis) and then `arccos(c · avgCAxes[fid])` (against whatever `AvgCAxes` was pre-populated for the feature). For a non-hex feature, the upstream `FindAvgCAxes` would either skip the feature (leaving `AvgCAxes[F] = 0` initialized) or produce arbitrary content; either way the resulting cell-level miso is not meaningful. +**Evidence:** The `Class 1 — Realistic Microstructure (exposes divide-by-zero)` and `Class 4 — Invariants` cases assert both results. The source comparison uses `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/FindFeatureReferenceCAxisMisorientations.cpp` at the DREAM.3D `v6.5.171` tag and the current SIMPLNX algorithm. The algorithm on all ten `upstream/release/v` branches from v7.0.0 through v7.4.2 contains the hexagonal gate. PR #566 (`6cd130eff`) introduced the gate; PR #1438 (`e6896714b`) changed aggregation; PR #1636 (`c225e8a24`) made NaN output explicit. -SIMPLNX adds the `isHex` gate at `Algorithms/ComputeFeatureReferenceCAxisMisorientations.cpp:124`: -```cpp -const bool isHex = crystalStructureType == ebsdlib::CrystalStructure::Hexagonal_High || crystalStructureType == ebsdlib::CrystalStructure::Hexagonal_Low; -... -if(isHex && cellFeatureId > 0 && cellPhase > 0) -{ - // c-axis projection -} -else -{ - cellRefCAxisMis.setValue(cellIdx, 0.0f); // explicit zero, skip accumulation -} -``` +--- -For a feature with all non-hex cells, `counts[featureId] = 0` after the per-cell loop, the per-feature avg becomes `0.0f / 0 = NaN` (IEEE 754) at line 176, and the per-feature stddev becomes `sqrt(NaN / 0) = NaN` at line 202. +### ComputeFeatureReferenceCAxisMisorientationsFilter-D2 -The fix was introduced in SIMPLNX via PR #1438 ("Microtexture related filter cleanup"); the same fix (`ENH: FindFeatureReferenceCAxisMisorientations - Update to use Eigen`, October 2025) was applied to a local build of the legacy source to prove the root cause. +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeFeatureReferenceCAxisMisorientationsFilter-D2` | +| **Filter UUID** | `16c487d2-8f99-4fb5-a4df-d3f70a8e6b25` | +| **Status** | active | -**Empirical confirmation (2026-06-10 A/B):** +**Symptom:** Cell angles and feature statistics can differ numerically from DREAM.3D v6.5.171 because intermediate precision and evaluation order differ. -Realistic-microstructure fixture, F3 = all-cubic (5 cells): +**Root cause:** Precision and library. DREAM.3D v6.5.171 uses float orientation matrices, `MatrixMath` vector operations, `acosf`, and a float array for the count, angle sum and squared-difference sum. SIMPLNX uses a double quaternion-to-matrix conversion, Eigen double vectors and a double angle calculation. Its feature-angle sum, feature mean and variance accumulator use double precision. It converts each stored float32 cell angle to double before subtracting the double feature mean. Output arrays use float32. -| Output | 6.5.171 (A) | Patched legacy (B) | SIMPLNX (C) | -|---------------------------------------------------------|--------------|--------------|--------------| -| `FeatureReferenceCAxisMisorientations[cells of F3]` | 9.999988 each cell | 0.0 each cell | 0.0 each cell | -| `FeatureAvgCAxisMisorientations[F3]` | 9.999988 | **NaN** | **NaN** | -| `FeatureStdevCAxisMisorientations[F3]` | 0.0 | **NaN** | **NaN** | +DREAM3D-NX v7.0.0 already uses double vector math. DREAM3D-NX v7.4.0 replaces the float variance accumulator with `std::vector` and changes cell traversal to plane, row, column order. Later releases retain these changes. This filter does not call `LaueOps::calculateMisorientation`; changes to that function do not explain this difference. -The 6.5.171 `9.999988°` value is the projection of the cubic cell's identity-Quat-derived c-axis `[0,0,1]` against the test fixture's `AvgCAxes[F3] = [0, sin(10°), cos(10°)]` — i.e., the test fixture happened to pre-populate F3's avg-c-axis at a 10° tilt, and 6.5.171 produces ~10° as the "miso" for each cubic cell. On a real dataset, that value would be whatever-random-content `FindAvgCAxes` left in `AvgCAxes[F3]`. +**Affected users:** Users who compare numerical outputs across DREAM.3D v6.5.171 and DREAM3D-NX or across the change in feature-statistics accumulation. -**Affected users:** Any workflow that runs `FindFeatureReferenceCAxisMisorientations` on mixed-phase datasets in DREAM3D 6.5.171. Single-phase hex-only datasets are unaffected. The garbage values propagate into downstream statistics (per-feature average + stddev rows for non-hex features) but cell-level visualization may not surface the issue immediately. +**Recommendation:** Trust SIMPLNX within the analytical tests' 0.001° tolerance. This tolerance applies to the analytical fixtures. -**Recommendation:** **Trust SIMPLNX.** The legacy 6.5.171 output for non-hex features is mathematically meaningless — the c-axis is a hex-specific concept and asking "how far is this cubic cell's c-axis from the feature's avg c-axis" has no defined answer. The `NaN` produced by SIMPLNX is the correct signal that the question is ill-posed for the feature. +**Evidence:** The three Class 1 cases independently check the cell angles, means and population standard deviations. PR #1438 (`e6896714b`) changed variance accumulation and traversal order. The release algorithm sources show double vector math in v7.0.0 and double variance accumulation from v7.4.0. --- -## ComputeFeatureReferenceCAxisMisorientationsFilter-D2 +## Part 2 — SIMPLNX defects and behavior changes -| Field | Value | -|------------------|--------------------------------------------------------------------------------------------------------------------------------------| -| **Deviation ID** | `ComputeFeatureReferenceCAxisMisorientationsFilter-D2` | -| **Filter UUID** | `16c487d2-8f99-4fb5-a4df-d3f70a8e6b25` | -| **Status** | active (SIMPLNX uses Eigen + double; legacy 6.5.171 uses hand-rolled MatrixMath + float32 — root cause proven by applying the same fixes to a local build of the legacy source) | +### ComputeFeatureReferenceCAxisMisorientationsFilter-D3 -**Symptom:** Per-cell `FeatureReferenceCAxisMisorientations` and per-feature `FeatureStdevCAxisMisorientations` values drift between SIMPLNX and DREAM3D 6.5.171 by `~1e-4°` to `~1e-6°`, with the larger magnitude appearing on per-cell values for cells whose Φ differs from the feature's avg Φ. The drift is precision-class, not algorithmic; both implementations compute the same analytical reduction. +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeFeatureReferenceCAxisMisorientationsFilter-D3` | +| **Filter UUID** | `16c487d2-8f99-4fb5-a4df-d3f70a8e6b25` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | -**Root cause:** **Precision + library.** Two stacked contributions: +**Symptom:** Successful execution omits warning `-9803` when non-hexagonal phases are skipped. -1. **Orientation-matrix math.** Legacy uses `MatrixMath::Transpose3x3` + `MatrixMath::Multiply3x3with3x1` + `MatrixMath::Normalize3x1` (all float32). SIMPLNX uses Eigen's `Eigen::Matrix3d.transpose()` + `Eigen::Vector3d` arithmetic (double precision throughout). The hand-rolled matrix math accumulates intermediate float32 round-off that the Eigen path does not. -2. **Standard-deviation accumulation.** Legacy accumulates `(diff * diff)` in a `std::vector` and divides by `static_cast(counts)`. SIMPLNX uses `std::vector` and `static_cast(counts)`, then casts the final result to float32 only at the output array write. The float32 accumulator on the legacy side lossily rounds each per-cell contribution. +**Root cause:** Bug. The affected algorithms append the warning to a local `Result<> result`, then return an empty result at the end. DREAM3D-NX v7.4.2 and the current filter return `result` and retain the warning. DREAM.3D v6.5.171 has no equivalent mixed-phase warning path. -The SIMPLNX-side fix has been present since the initial port (no SIMPLNX-side code change required for this V&V cycle). The legacy-side root-cause proof is split across two surgical fixes applied to a local build of the legacy source: +**Affected users:** Users who depend on execution warnings to identify skipped non-hexagonal phases. The separate preflight warning `-9801` is not affected. -- `ENH: FindFeatureReferenceCAxisMisorientations - Update to use Eigen` (October 2025) — replaces `MatrixMath::*` with Eigen for the per-cell orientation-matrix → c-axis pipeline. -- `BUG: FindFeatureReferenceCAxisOrientation - Use doubles to accumulate the StdDev values` (October 2025) — promotes the stddev accumulator from `std::vector` to `std::vector` and uses `double` for `diff` and the final division. +**Recommendation:** Trust SIMPLNX. The returned result now carries the warning that the algorithm creates. -With both fixes applied, the patched legacy build produces byte-identical output to SIMPLNX across all 3 output arrays on the FRCAM A/B fixture. +**Evidence:** PR #1636 (`c225e8a24`) changes the final statement to `return result;`. The release sources through v7.4.1 discard the warning; v7.4.2 returns it. `Realistic Microstructure` exercises mixed phases, but its nonempty-warning assertion does not isolate `-9803` from the preflight warning. -**Empirical confirmation (2026-06-10 A/B):** - -Per-cell `FeatureReferenceCAxisMisorientations`, sample of hex-feature cells (F2 row, cell Φ = 8°, 9°, 10°, 11°, 12°, avg Φ = 10°): +--- -| Cell | Expected | 6.5.171 (A) | Patched legacy (B) | SIMPLNX (C) | -|------|----------|--------------|--------------|--------------| -| F2[0] | 2.0 | 1.999782 | 2.000001 | 2.000001 | -| F2[1] | 1.0 | 0.999755 | 1.000000 | 1.000000 | -| F2[2] | 0.0 | 0.000000 | 0.000001 | 0.000001 | -| F2[3] | 1.0 | 0.999755 | 1.000000 | 1.000000 | -| F2[4] | 2.0 | 1.999880 | 2.000000 | 2.000000 | +### ComputeFeatureReferenceCAxisMisorientationsFilter-D4 -Per-feature `FeatureAvgCAxisMisorientations`: +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeFeatureReferenceCAxisMisorientationsFilter-D4` | +| **Filter UUID** | `16c487d2-8f99-4fb5-a4df-d3f70a8e6b25` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | The DREAM3D-NX release after v7.4.2 | -| Feature | Expected | 6.5.171 (A) | Patched legacy (B) ≡ SIMPLNX (C) | -|---------|-----------|--------------|---------------------------| -| F1 | 0.0 | 0.000000 | 0.000000 | -| F2 | 1.2 | 1.199834 | 1.200000 | -| F4 | 0.0 | 0.000000 | 0.000000 | -| F5 | 2.8 | 2.800004 | 2.800000 | +**Symptom:** An invalid phase can read outside Crystal Structures, including on a cell that should be skipped. -Per-feature `FeatureStdevCAxisMisorientations`: +**Root cause:** Bug. DREAM3D-NX v7.0.0 through v7.4.2 read `crystalStructures[cellPhases[cellIdx]]` before testing whether the cell participates. No phase-range check precedes that read. The current filter first requires a positive FeatureId and phase, then checks the phase against the ensemble tuple count. An invalid positive participating phase returns `-9804`; a nonpositive phase or feature zero skips the lookup. DREAM.3D v6.5.171 does not read Crystal Structures in this filter, so it has no such lookup. -| Feature | Expected | 6.5.171 (A) | Patched legacy (B) ≡ SIMPLNX (C) | -|---------|--------------|--------------|---------------------------| -| F2 | √0.56 ≈ 0.7483 | 0.748285 | 0.748331 | -| F5 | √3.76 ≈ 1.9391 | 1.939061 | 1.939072 | +**Affected users:** Users whose Cell Phases array contains an invalid ensemble index. -**Affected users:** Any workflow that runs `FindFeatureReferenceCAxisMisorientations` on 6.5.171 and compares its output against SIMPLNX. The drift magnitude (~1e-4° per cell, ~1e-5° per feature avg) is sub-perceptual for visualization but visible in numerical analysis or unit-test comparisons. Real-world EBSD datasets with many cells per feature will see the per-feature avg drift become more stable (the float32-noise per-cell contributions average toward the analytical mean), but the per-cell values themselves still show the precision-class drift. +**Recommendation:** Trust SIMPLNX. Correct the phase index or supply the corresponding ensemble entry. -**Recommendation:** **Trust SIMPLNX.** The SIMPLNX value is closer to the analytical oracle. The 6.5.171 value differs by precision-class round-off, not by an algorithmic choice. +**Evidence:** PR #1568 (`4ec21e93f`) adds the participation and phase-range checks. The release algorithm sources from v7.0.0 through v7.4.2 perform the lookup before those checks. `Phase Index Bounds` checks the error and the two skip conditions. All visible tests pass in CI at `419f9db35`. diff --git a/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFeatureReferenceMisorientationsFilter.md b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFeatureReferenceMisorientationsFilter.md index 063354d574..b609ed86df 100644 --- a/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFeatureReferenceMisorientationsFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFeatureReferenceMisorientationsFilter.md @@ -1,70 +1,157 @@ -# Deviations from DREAM3D 6.5.171: ComputeFeatureReferenceMisorientationsFilter +# Deviations from DREAM.3D v6.5.171: ComputeFeatureReferenceMisorientationsFilter -This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent (`FindFeatureReferenceMisorientations`, source at `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/FindFeatureReferenceMisorientations.{h,cpp}` in DREAM3D 6.5.171). +Part 1 lists differences from DREAM.3D v6.5.171 (D1, D2 and D7). Part 2 lists SIMPLNX defects (D3 through D6). Both parts use one stable ID sequence; the Filter UUID is the permanent cross-reference anchor. -Entries are referenced by stable ID (`ComputeFeatureReferenceMisorientationsFilter-D`) from the V&V report and from public migration guidance. The ID is stable across renames; the Filter UUID field is the permanent cross-reference anchor. +The source and comparison evidence is in `../provenance/ComputeFeatureReferenceMisorientationsFilter.md`. The recorded comparison predates the current block-read algorithm. -## Comparison summary +--- + +## Part 1 — Differences from DREAM.3D v6.5.171 + +### ComputeFeatureReferenceMisorientationsFilter-D1 + +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeFeatureReferenceMisorientationsFilter-D1` | +| **Filter UUID** | `24b54daf-3bf5-4331-93f6-03a49f719bf1` | +| **Status** | active | + +**Symptom:** Cell reference angles and feature means differ from DREAM.3D v6.5.171 because orientation precision and accumulation order differ. -The legacy A/B comparison was performed by **source inspection** rather than empirical run. Justification: SIMPLNX `ComputeFeatureReferenceMisorientationsFilter::operator()()` is a clean Port of legacy `FindFeatureReferenceMisorientations::execute()` (same two-mode dispatch, same per-voxel main loop, same per-feature averaging finalization, same `LaueOps`-delegated misorientation math, same `m_Centers` selection with `>=` tie-break). The only port-time deltas are API modernization (`getMisoQuat` → `calculateMisorientation`), type widening (`QuatF` → `QuatD` for the internal math, narrowed back to `float` for storage), cleaner auxiliary storage (interleaved `avgMisoPtr` → separate `avgMisorientationSums` + `avgMisorientationCounts`), added cancel checks, and a new optional `EuclideanCenters` output array in Mode 1 (no pre-existing output is affected). +**Root cause:** Precision and order of operations. DREAM.3D v6.5.171 uses `QuatF` and `getMisoQuat`; SIMPLNX uses `QuatD` and `calculateMisorientation`. Both accumulate float32 feature sums. DREAM.3D visits column, row, then plane; SIMPLNX visits increasing linear indices. Floating-point addition depends on this order. An arithmetic mean does not amplify a uniform per-cell error by the cell count. -For the V&V data fixtures (pure φ1 rotations about z, no cubic-symmetry-op-aligned grain boundaries), both implementations are expected to produce bit-identical outputs modulo `float` precision (< 1 ULP differences possible due to `QuatF` → `QuatD` promotion). +**Affected users:** Users who compare floating-point angles or feature means across applications, including cubic-phase EBSD data. -For real EBSD data (e.g., the Small-IN100 dataset that the retired exemplar archive came from), the EbsdLib 2.4.1 `CubicOps::calculateMisorientationInternal` precision improvement (`2·atan2(|v|, w)` form replacing the precision-fragile `acos(w)` near 1) propagates through SIMPLNX's misorientation math and yields per-voxel `FRM` values that differ from legacy by ~ULP-scale (sub-`0.0001°`) for sym-op-aligned voxel pairs. When those per-voxel values are averaged into per-feature `avgRefMis` quantities, the small per-voxel shifts can accumulate to 2×–10× the `1e-4` epsilon used by the retired exemplar tests. +**Recommendation:** Trust SIMPLNX. The analytical 5° fixtures support its angle values; compare feature means with a tolerance that accounts for float32 accumulation. + +**Evidence:** The comparison uses SIMPLNX `8b80721c2` and predates the block-read algorithm in `4ec21e93f`. The 2026-09-17 run gives maximum Small IN100 Mode 0 differences of 0.07380° per cell and 0.01761° per feature between SIMPLNX and DREAM.3D v6.5.171. Fixture B differs by 3.052e-5° and fixture C by 1.864e-4°. A local proof version reduces the Small IN100 per-cell difference to 1.907e-6°; the feature difference is 1.669e-5°. Recomputing each feature sum in its application's traversal order reproduces its stored mean exactly. See `vv_work:ComputeFeatureReferenceMisorientationsFilter/comparison/results.md` and `vv_work:ComputeFeatureReferenceMisorientationsFilter/algorithm_review/source_comparison.md`. --- -## ComputeFeatureReferenceMisorientationsFilter-D1 +### ComputeFeatureReferenceMisorientationsFilter-D2 + +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeFeatureReferenceMisorientationsFilter-D2` | +| **Filter UUID** | `24b54daf-3bf5-4331-93f6-03a49f719bf1` | +| **Status** | active | -| Field | Value | -|------------------|-------------------------------------------------------------| -| **Deviation ID** | `ComputeFeatureReferenceMisorientationsFilter-D1` | -| **Filter UUID** | `24b54daf-3bf5-4331-93f6-03a49f719bf1` | -| **Status** | active (precision-class; non-deviation in algorithmic sense) | +**Symptom:** Direct Mode 1 can fail before calculating misorientations because it accesses the average-quaternion input that only Mode 0 requires. -**Symptom:** Per-feature average misorientations (`Feature Avg Misorientations`) differ between SIMPLNX and DREAM3D 6.5.171 on real EBSD datasets containing cubic-phase grains with grain boundaries near cubic-symmetry operators. On the Small-IN100 dataset (the basis for the retired `compute_feature_reference_misorientation.tar.gz` exemplar archive), Mode 0 (`AverageMisorientation`) averages drifted by ~`2e-4` (2× the `1e-4` epsilon used by `CompareDataArrays`); Mode 1 (`EuclideanDistance`) averages drifted by ~`1e-3` (10×). Per-voxel `FRM` values shift by sub-`0.0001°` (within float precision), but the magnitude amplifies when summed over a feature's voxels and divided by count. +**Root cause:** Bug. DREAM.3D v6.5.171 initializes `m_AvgQuatsPtr` only in Mode 0, then dereferences it for `totalFeatures` in either mode. Released SIMPLNX also required the AvgQuats array unconditionally. SIMPLNX replaces that required lookup with an optional pointer and obtains the feature count from the selected feature matrix when AvgQuats is absent. -On the V&V data fixtures (pure φ1 rotations about z, no cubic-sym-op-aligned voxel pairs), no observable deviation. All 6 Class 1 fixtures produce SIMPLNX values within `1e-3°` of the analytical expected value. +**Affected users:** Users of DREAM.3D v6.5.171 or DREAM3D-NX v7.0.0 through v7.4.0 who select the farthest-boundary orientation without supplying the Mode 0 average-quaternion input. -**Root cause:** **Precision** — not an algorithm change in either implementation. +**Recommendation:** Trust SIMPLNX. DREAM3D-NX v7.4.1 contains the fix. Mode 1 can use the selected feature matrix independently of AvgQuats. -The deviation traces to the EbsdLib 2.4.1 release commit `5c8c993` (BlueQuartz Software, 2026-05-29), which replaces a precision-fragile `acos(w)` form in `CubicOps::calculateMisorientationInternal` with a numerically-stable `2·atan2(|v|, w)` form using the explicit reduced-quaternion `v` components. The precision improvement is real and mathematically more correct; it manifests for cubic misorientations whose minimum-rotation-axis representation lies on or near a cubic symmetry operator (e.g., 90° about the cubic c-axis is a 4-fold sym op of m-3m; pre-fix `acos`-form yielded `~0.02°` residual due to float32-input ULP noise, post-fix yields the mathematically correct value). +**Evidence:** PR #1504 (`f7fec9819`) fixes the required AvgQuats lookup in SIMPLNX. On 2026-09-17, all four direct Mode 1 cases crashed with exit 139 in DREAM.3D v6.5.171. A local proof version obtains the count from the always-created feature-average output; all four cases complete. Its three analytical cases match SIMPLNX exactly. See `vv_work:ComputeFeatureReferenceMisorientationsFilter/ReadMe.md`, `vv_work:ComputeFeatureReferenceMisorientationsFilter/comparison/run_exit_codes.txt` and `vv_work:ComputeFeatureReferenceMisorientationsFilter/comparison/results.md`. Current unit fixtures create AvgQuats in both modes and do not isolate its absence. + +--- -This filter is a clean Port of `FindFeatureReferenceMisorientations` from DREAM3D 6.5.171; the SIMPLNX algorithm reproduces the legacy two-mode dispatch + per-voxel misorientation accumulation + per-feature averaging structure exactly. The legacy filter consumes `OrientationLib::CubicOps::getMisoQuat` (pre-fix `acos`-form, float32); the SIMPLNX filter consumes `ebsdlib::CubicOps::calculateMisorientation` (post-fix `2·atan2`-form, QuatD). The difference is entirely in the EbsdLib precision improvement, NOT in this filter. +### ComputeFeatureReferenceMisorientationsFilter-D7 -For the full root-cause walkthrough of the EbsdLib precision improvement, see the precedent characterization in `vv/deviations/BadDataNeighborOrientationCheckFilter.md` §"Non-deviations" → "EbsdLib 2.4.1 CubicOps precision improvement". The characterization there applies equally to this filter; the only difference is that this filter's per-feature averaging amplifies the per-voxel precision shift across the feature's voxels (typically hundreds to thousands for real EBSD data), making the deviation more visible at the per-feature output level than at the per-voxel level. +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeFeatureReferenceMisorientationsFilter-D7` | +| **Filter UUID** | `24b54daf-3bf5-4331-93f6-03a49f719bf1` | +| **Status** | active | -**Affected users:** Anyone running this filter in DREAM3D 6.5.171 on EBSD data with cubic-phase grains that have grain boundaries near 4-fold (90° c-axis), 3-fold (120° [111]), or 2-fold (180° face-diagonal) cubic symmetry operators, and comparing per-feature `Feature Avg Misorientations` output across the version boundary. On non-cubic-phase data, no deviation. On cubic data without sym-op-aligned boundaries, no observable deviation. +**Symptom:** SIMPLNX writes feature reference-cell coordinates in Mode 1; DREAM.3D v6.5.171 has no equivalent output array. -**Recommendation:** **Trust SIMPLNX.** The 6.5.171 result was limited by float32-input ULP noise amplified by `acos`-near-1 catastrophic cancellation; SIMPLNX returns the mathematically correct value. The `~0.02°` shift is well below typical EBSD measurement resolution (per the BadDataNeighborOrientationCheckFilter V&V cycle's precedent characterization) and will not materially affect downstream microstructural analyses for users migrating from DREAM3D 6.5.171. +**Root cause:** Algorithmic choice. SIMPLNX adds the Euclidean Centers output. The algorithm converts the selected linear cell index to the physical cell-center position. This extra output does not change the reference quaternion. + +**Affected users:** Users who need to locate or display the reference cell for each feature. + +**Recommendation:** Trust SIMPLNX. The analytical Mode 1 fixtures check literal 2D and 3D coordinates. + +**Evidence:** PR #1504 adds the Euclidean Centers output. The DREAM.3D v6.5.171 source creates only the two misorientation outputs. The 2026-09-17 results also record the absent legacy coordinate array in `vv_work:ComputeFeatureReferenceMisorientationsFilter/comparison/results.md`. --- -## Non-deviations (algorithm characteristics common to both filters) +## Part 2 — SIMPLNX defects and behavior changes -The following behaviors are NOT deviations — SIMPLNX and 6.5.171 agree on them. Captured here so future engineers don't re-discover them and propose them as deviations. +### ComputeFeatureReferenceMisorientationsFilter-D3 -### Raster-order tie-break in `centers[]` selection (Mode 1) +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeFeatureReferenceMisorientationsFilter-D3` | +| **Filter UUID** | `24b54daf-3bf5-4331-93f6-03a49f719bf1` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.0. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.1 | -Both implementations use `if(distance >= centerDistances[featureId])` in the Mode 1 pre-loop that selects each feature's reference voxel. The `>=` (rather than `>`) means that when two or more voxels within a feature have identical `GBEuclideanDistances` values, the LATER voxel (in linear iteration order) overwrites earlier candidates and is selected as the feature's reference. The choice is therefore raster-order dependent — different DataStructure layouts that expose the same logical voxels in a different iteration order would yield different `centers[]` and different `EuclideanCenters`. **Both filters share this behavior** — verified by source inspection of the legacy `FindFeatureReferenceMisorientations::execute()` lines 320-325 vs SIMPLNX `ComputeFeatureReferenceMisorientations.cpp` lines 89-103. +**Symptom:** Mode 1 can calculate wrong angles or access outside the feature-average quaternion array. -### Background voxel and unphased voxel handling +**Root cause:** Bug. The old SIMPLNX code uses `avgQuats[centerGNum * 4 + ...]`, although `centerGNum` is a cell index. SIMPLNX now reads the selected cell quaternion. The current algorithm caches that quaternion in `centerQuats`. DREAM.3D v6.5.171 uses `quats[m_Centers[gnum]]` for this operation; its separate Mode 1 crash is D2. -Both implementations skip voxels where `featureIds[i] == 0` (background) or `cellPhases[i] == 0` (unphased). In both, the per-voxel `FRM` array is initialized to 0 (or `fill(0.0f)` in SIMPLNX) and skipped voxels retain that zero value. Per-feature `avgRefMis` is computed only over the non-skipped voxels in each feature; if a feature consists entirely of skipped voxels, its `count == 0` and `avgRefMis[fid]` is set to `0`. **Both filters share this behavior** — algorithm characteristic, not a defect. +**Affected users:** Users who select the farthest-boundary reference in the affected releases. -### Background feature (featureId = 0) → `avgRefMis[0] == 0` +**Recommendation:** Trust SIMPLNX. The reference quaternion must come from the selected cell. -Both implementations leave `avgRefMis[0]` at its initialized `0.0f` value (since the main per-voxel loop only computes misorientations for `featureIds[i] > 0`, and the per-feature finalize loop iterates `for(i = 1; i < totalFeatures; i++)`, skipping index 0 entirely). Algorithm characteristic, not a defect. +**Evidence:** PR #1504 (`f7fec9819`) fixes the reference quaternion lookup. `Class 1 - Mode 1 KnownCenter`, `Class 1 - Mode 1 MultiGrain CenterIsolation` and `Class 1 - Mode 1 3D Volume` check cell angles and feature means from literal expectations. --- -## Comparison artifacts +### ComputeFeatureReferenceMisorientationsFilter-D4 + +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeFeatureReferenceMisorientationsFilter-D4` | +| **Filter UUID** | `24b54daf-3bf5-4331-93f6-03a49f719bf1` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | + +**Symptom:** An imported SIMPL Mode 1 pipeline selects the cell matrix where it requires the feature matrix. + +**Root cause:** Bug. `FromSIMPLJson()` derived `k_CellFeatureAttributeMatrixPath_Key` from `FeatureIdsArrayPath`. The conversion now derives it from `AvgQuatsArrayPath`, which identifies the feature matrix. DREAM.3D v6.5.171 does not use the SIMPLNX conversion path. + +**Affected users:** Users who import legacy pipelines with distinct cell and feature matrices and select Mode 1. + +**Recommendation:** Trust SIMPLNX. The conversion now uses the feature-level path. + +**Evidence:** PR #1682 (`5eb69f359`) fixes the feature-matrix source path. The current conversion test checks arguments, but both fixtures put AvgQuats and FeatureIds under the same parent. It does not distinguish the corrected source path from the old one. + +--- + +### ComputeFeatureReferenceMisorientationsFilter-D5 + +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeFeatureReferenceMisorientationsFilter-D5` | +| **Filter UUID** | `24b54daf-3bf5-4331-93f6-03a49f719bf1` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was affected. | +| **Fixed in** | The DREAM3D-NX release after v7.4.2 | + +**Symptom:** A positive participating phase outside the Crystal Structures array can cause an out-of-bounds read. + +**Root cause:** Bug. Released SIMPLNX reads `crystalStructures[cellPhases[voxelIdx]]` without a phase-range check. DREAM.3D v6.5.171 also performs an unchecked lookup. The current algorithm checks the positive phase before reading the ensemble cache and returns `-34901` on failure. + +**Affected users:** Users whose positive phase values do not identify a Crystal Structures tuple on a participating cell. + +**Recommendation:** Trust SIMPLNX. Correct the phase or supply the corresponding ensemble tuple before execution. + +**Evidence:** PR #1568 (`4ec21e93f`) adds the phase-range check. `Phase and Laue Index Bounds` checks phase 2 against a two-tuple ensemble in both modes. It also checks that an invalid phase on feature zero is ignored. + +--- + +### ComputeFeatureReferenceMisorientationsFilter-D6 + +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeFeatureReferenceMisorientationsFilter-D6` | +| **Filter UUID** | `24b54daf-3bf5-4331-93f6-03a49f719bf1` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was affected. | +| **Fixed in** | The DREAM3D-NX release after v7.4.2 | + +**Symptom:** A participating crystal-structure value outside the Laue-operator range can cause an invalid operator access. + +**Root cause:** Bug. Released SIMPLNX passes the Crystal Structures value directly to the operator vector. DREAM.3D v6.5.171 does the same. The current algorithm checks the value against `orientationOps.size()` and returns `-34902` before the lookup. -For this filter's V&V cycle, the legacy A/B comparison was performed by **source inspection** rather than empirical run. Justification: both algorithms have been independently V&V'd at the source-code level (this filter via the V&V report; the EbsdLib precision math via BadDataNeighborOrientationCheckFilter's V&V cycle), and the data fixtures used here do not include sym-op-aligned boundaries that would surface the precision-class deviation. Running an empirical A/B on the data fixtures would confirm bit-identical (or sub-ULP) output, which is the expected outcome from source inspection. +**Affected users:** Users whose participating phase has an invalid Crystal Structures value. -If a future engineer wants to run an empirical A/B for confirmation: +**Recommendation:** Trust SIMPLNX. Supply a valid Laue index for each participating phase. -- **6.5.171 binary**: `/Users/mjackson/Applications/DREAM3D.app/Contents/bin/PipelineRunner` -- **Suggested input fixture**: convert any V&V data fixture to legacy `.dream3d` format via the same h5py-based script pattern used in `BadDataNeighborOrientationCheckFilter`'s `bad_data_neighbor_orientation_check_v2/case_1/.../6_5_*_input.json` series. A draft Python script for Fixture B (Mode 0, 2×2×2 single grain, all 5° about z) lives at `/tmp/build_cfrm_fixtureB_legacy.py` from this V&V cycle. -- **Suggested legacy pipeline**: `DataContainerReader` → `FindFeatureReferenceMisorientations` → `DataContainerWriter`. The `DataContainerReader` requires a `DataContainerArrayProxy` enumerating the input file's paths; that adds ~150 lines of JSON for a 6-data-fixture sweep. -- **Expected outcome**: bit-identical or sub-ULP-difference output between SIMPLNX and 6.5.171 on the data fixtures (no sym-op-aligned boundaries → precision improvement not observable). +**Evidence:** PR #1568 (`4ec21e93f`) adds the Laue-index check. `Phase and Laue Index Bounds` checks an invalid Laue value in both reference modes. diff --git a/src/Plugins/OrientationAnalysis/vv/deviations/ComputeIPFColorsFilter.md b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeIPFColorsFilter.md index fac9cc4804..7b5023e9d4 100644 --- a/src/Plugins/OrientationAnalysis/vv/deviations/ComputeIPFColorsFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeIPFColorsFilter.md @@ -1,12 +1,14 @@ -# Deviations from DREAM3D 6.5.171: ComputeIPFColorsFilter +# Deviations from DREAM.3D v6.5.171: ComputeIPFColorsFilter -This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent (`GenerateIPFColors`). +Part 1 records the TSL color difference from DREAM.3D v6.5.171 (D1). Part 2 records two fixed implementation defects (D2 and D3). -Entries are referenced by stable ID (`ComputeIPFColorsFilter-D`) from the V&V report and from public migration guidance. The ID is stable across renames; the Filter UUID field is the permanent cross-reference anchor. +Entries use the stable ID sequence `ComputeIPFColorsFilter-D`. The Filter UUID is the permanent cross-reference anchor. Source and comparison evidence are in `../provenance/ComputeIPFColorsFilter.md`. --- -## ComputeIPFColorsFilter-D1 +## Part 1 — Differences from DREAM.3D v6.5.171 + +### ComputeIPFColorsFilter-D1 | Field | Value | |---|---| @@ -14,16 +16,60 @@ Entries are referenced by stable ID (`ComputeIPFColorsFilter-D`) from the V&V | **Filter UUID** | `64cb4f27-6e5e-4dd2-8a03-0c448cb8f5e6` | | **Status** | active | -**Symptom:** On the TSL color scheme, a very small fraction of cells (14 of 343,963, or 0.004%, on the single-phase cubic `so3_cubic_high_ipf_001` dataset) receive an IPF color that differs from a freshly-run DREAM3D 6.5.171 by exactly ±1 in one of the three 8-bit RGB channels. +**Symptom:** On the 343,963-cell cubic fixture, DREAM3D-NX v7.4.1 and v7.4.2 each differ from DREAM.3D v6.5.171 at 14 cells. Each difference is one level in one 8-bit RGB channel. + +**Root cause:** Precision and library. DREAM.3D v6.5.171 uses OrientationLib; SIMPLNX uses EbsdLib. Both calculate TSL colors and pack them into integer RGB channels. Small differences in the library calculation can change the integer at a quantization boundary. Both tested DREAM3D-NX releases match the embedded legacy color array exactly. + +**Affected users:** Users who compare TSL color arrays byte for byte with DREAM.3D v6.5.171 output. + +**Recommendation:** Either result is acceptable within one 8-bit channel level on this fixture. Use that tolerance for this comparison; do not treat the stored legacy colors as an independent correctness oracle. + +**Evidence:** Run against DREAM.3D v6.5.171 on 2026-09-17: 343,949 of 343,963 RGB tuples match exactly for each tested DREAM3D-NX release; all tuples match within one channel level. See `vv_work:ComputeIPFColorsFilter/README.md` and `vv_work:ComputeIPFColorsFilter/results/release_matrix/comparison.json`. These results describe v7.4.1 and v7.4.2, not the current algorithm. + +DREAM.3D v6.5.171 supports only TSL. SIMPLNX also supplies PUCM and Nolze-Hielscher color keys. The current `ColorKey choice reaches algorithm` test checks that each additional key differs from TSL. + +--- + +## Part 2 — SIMPLNX defects and behavior changes + +### ComputeIPFColorsFilter-D2 + +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeIPFColorsFilter-D2` | +| **Filter UUID** | `64cb4f27-6e5e-4dd2-8a03-0c448cb8f5e6` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | + +**Symptom:** Parallel processing of multiple out-of-range phase IDs caused a data race in the error counter and could lose increments. + +**Root cause:** Bug. Workers called `incrementPhaseWarningCount()` on one shared `int32_t`. The increment had no synchronization. `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeIPFColorsDirect.hpp` now declares `std::atomic_int32_t`; the Direct worker increments that atomic counter. Scanline counts invalid phases in its serial loop. -**Root cause:** *precision + library.* IPF coloring is delegated to a crystallographic library — legacy DREAM3D uses its in-tree **OrientationLib**, SIMPLNX uses **EbsdLib**. Both implement the same standard TSL coloring and agree on the continuous RGB value to well within 1/255, but for the handful of orientations whose computed channel value lands right on a `×255` quantization boundary (observed at 57↔58, 130↔131, 164↔165, …) the minute intermediate-math differences between the two libraries tip the final `static_cast` by one integer level. The SIMPLNX orchestration (`Algorithms/ComputeIPFColors.cpp`) is otherwise a line-for-line port of `GenerateIPFColors::execute()`; it does not itself perform the color math. Notably, SIMPLNX reproduces the legacy `IPF Colors` array **stored inside the input file byte-for-byte** — the ±1 cells appear only against a fresh run of one particular 6.5.171 binary, confirming the difference is quantization jitter, not an algorithmic change. +**Affected users:** Users with phase IDs at or above the ensemble tuple count during parallel execution. -**Affected users:** Anyone diffing SIMPLNX IPF-color output against a specific 6.5.171 build at the single-bit level. Visually, and for every downstream use, the images are identical; a ±1/255 channel difference on 0.004% of cells is imperceptible. +**Recommendation:** Use DREAM3D-NX v7.4.2 or later. Correct the invalid phase IDs reported by error `-48000`. -**Recommendation:** *either acceptable within tolerance ±1/255.* Neither result is more correct — both are the same continuous color quantized to 8 bits, differing only in rounding direction at the boundary. No legacy patch is warranted (the 6.5.171 output is not wrong). +**Evidence:** The released source uses a plain counter through v7.4.1 and an atomic counter in v7.4.2. DREAM.3D v6.5.171 also increments a plain counter from parallel workers. The current `phase index out of range returns -48000` test checks the error code for one invalid cell; it does not test concurrent increments. --- -## Note on color schemes with no legacy equivalent +### ComputeIPFColorsFilter-D3 + +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeIPFColorsFilter-D3` | +| **Filter UUID** | `64cb4f27-6e5e-4dd2-8a03-0c448cb8f5e6` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was affected. | +| **Fixed in** | The DREAM3D-NX release after v7.4.2 | + +**Symptom:** An enabled cell with a negative phase ID could cause an out-of-bounds read from Crystal Structures. + +**Root cause:** Bug. The coloring guard checked only `phase < numPhases` before indexing `crystalStructures[phase]`. A negative value passed this check. Both current executors reject an enabled negative phase with `-48001` before the lookup. A masked negative phase does not participate. + +**Affected users:** Users whose Phases array contains negative values at enabled cells, including all cells when masking is disabled. + +**Recommendation:** Use the corrected filter and replace invalid phase IDs with valid ensemble indices. A mask can exclude cells that must not participate. -DREAM3D 6.5.171 `GenerateIPFColors` produced **only** the TSL (EDAX/OIM) color scheme. SIMPLNX adds a `Color Key` choice exposing two additional EbsdLib schemes — **PUCM** (Patala / MTEX-style perceptually-uniform) and **Nolze-Hielscher** (MTEX HSV-style). These have no DREAM3D 6.5.171 counterpart, so no legacy comparison is possible or meaningful for them; they are a new feature, not a deviation. Their per-Laue-class correctness is covered upstream by EbsdLib's `PUCMColorKeyTest` and `NolzeHielscherColorKeyTest`. +**Evidence:** All released DREAM3D-NX sources from v7.0.0 through v7.4.2 contain the unchecked lower bound. DREAM.3D v6.5.171 has the same guard. `src/Plugins/OrientationAnalysis/test/ComputeIPFColorsTest.cpp::OrientationAnalysis::ComputeIPFColorsFilter: negative Phase bounds` checks `-48001` without a mask and success when a boolean mask excludes the negative phase. diff --git a/src/Plugins/OrientationAnalysis/vv/deviations/ComputeKernelAvgMisorientationsFilter.md b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeKernelAvgMisorientationsFilter.md index aa9eb96a44..97db139d7d 100644 --- a/src/Plugins/OrientationAnalysis/vv/deviations/ComputeKernelAvgMisorientationsFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeKernelAvgMisorientationsFilter.md @@ -1,171 +1,113 @@ -# Deviations from DREAM3D 6.5.171: ComputeKernelAvgMisorientationsFilter +# Deviations from DREAM.3D v6.5.171: ComputeKernelAvgMisorientationsFilter -This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent (`FindKernelAvgMisorientations`, source at `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/FindKernelAvgMisorientations.{h,cpp}` in DREAM3D 6.5.171). +Part 1 lists differences from DREAM.3D v6.5.171 (D1–D3). Part 2 lists fixed input-safety defects (D4–D5). The stable IDs share one sequence, and the Filter UUID is the permanent cross-reference anchor. -Entries are referenced by stable ID (`ComputeKernelAvgMisorientationsFilter-D`) from the V&V report and from public migration guidance. The ID is stable across renames; the Filter UUID field is the permanent cross-reference anchor. - -## Comparison summary - -The legacy A/B comparison was performed by source inspection (2026-06-03) **and by an empirical runtime A/B on the default per-grain path** (2026-07-15, branch `topic/kam_ignore_feature_ids`). SIMPLNX `ComputeKernelAvgMisorientations` is a clean Port of legacy `FindKernelAvgMisorientations::execute()` (same per-voxel outer triple loop; same per-kernel inner triple loop; same focal-validity gate; on the default path the same same-feature gate inside the kernel — legacy line 292 ≡ SIMPLNX line 124 `true`-branch; same per-voxel average with the focal voxel always included in the divisor). The port-time deltas are documented in the V&V report's Algorithm Relationship section. Three deviations are recorded: a precision-class non-deviation (D1) traceable to the EbsdLib 2.4.1 release **and now empirically quantified** (see D1's runtime-A/B block); a legacy bug (D2) at the inner x-loop bound corrected at port time; and an NX-only capability (D3), the `use_feature_ids=false` per-voxel mode added for issue #1613, which has no legacy counterpart and is therefore validated by oracle only, not by comparison. - -**Runtime A/B setup.** A synthetic legacy-format input (`.superpowers/sdd/task-6-ab/kam_ab_input.dream3d`, authored with the `compare-legacy-dream3d` writer helper) — a 12×12×12 image, 8 features (2×2×2 octant blocks), single cubic phase, per-cell orientations = per-feature base rotation ⊗ ≤3° intra-grain perturbation — was fed **unchanged** through both binaries: `PipelineRunner` (6.5.171 `FindKernelAvgMisorientations`) via `legacy_kam.json`, and `nxrunner` (`ComputeKernelAvgMisorientations`, `use_feature_ids=true`) via `nx_kam.d3dpipeline` (nxrunner imports the legacy v7 file directly). Kernel `{1,1,1}` (default; D2 dormant). The round-tripped `Quats` and `FeatureIds` were verified bit-identical in both outputs, so any KAM difference is algorithmic/numeric, not an input artifact. +Source, release and comparison evidence is recorded in `../provenance/ComputeKernelAvgMisorientationsFilter.md`. --- -## ComputeKernelAvgMisorientationsFilter-D1 - -| Field | Value | -|------------------|----------------------------------------------------------------------------------| -| **Deviation ID** | `ComputeKernelAvgMisorientationsFilter-D1` | -| **Filter UUID** | `61cfc9c1-aa0e-452b-b9ef-d3b9e6268035` | -| **Status** | active (precision-class; non-deviation in algorithmic sense) | - -**Symptom:** Per-cell `KernelAverageMisorientations` values differ between SIMPLNX built against fixed EbsdLib (≥ v2.4.1, commit `5c8c993`) and DREAM3D 6.5.171 (and, equivalently, between fixed-EbsdLib and pre-fix-EbsdLib SIMPLNX builds — see the dependency note below). The shift is **quaternion-specific, not a uniform per-cell offset**: it is *exactly* 0 for focal cells whose symmetry-reduced self-misorientation lands on the trivial `wmin` candidate (the identity quaternion, and small rotations about a high-symmetry axis), and ~`0.03°` only for focal cells whose self-misorientation is reduced through a non-trivial cubic sym-op candidate (4-fold / 3-fold / 2-fold) that lands at `1 − ε`. Across a real dataset (Small_IN100 and similar) this averages to a per-cell shift of ~`0.005–0.05°`, depending on the focal-cell orientation distribution and the kernel size; it amplifies for asymmetric (e.g. `{2,2,1}`) or single-voxel kernels because the focal-cell self-misorientation term then carries more weight in the average. - -**Dependency:** the fix lives in EbsdLib commit `5c8c993`, contained in the `v2.4.1` tag. As of this PR the SIMPLNX `vcpkg.json` pins `ebsdlib version>=2.4.1`, so the **standard vcpkg build now links the fixed EbsdLib** and the artifact no longer appears in any supported configuration: both the standard build (`NX-Com-Qt69-Vtk95-Rel`) and the local-source build (`NX-Com-Qt69-Vtk95-Rel-EbsdLib`, `SIMPLNX_USE_LOCAL_EBSD_LIB=ON`) produce the correct, self-miso-free result. The V&V data-fixture unit tests assert the exact analytical oracle (margin `1e-3`) and pass in both configurations. The artifact reappears only if EbsdLib is pinned below `2.4.1` (e.g. an older vcpkg baseline); the *Empirical confirmation* below was captured against the pre-fix `2.4.0` — the version that shipped before this PR — to characterize the symptom and verify the fix. - -**Root cause:** **Precision** — not an algorithm change in either implementation. - -The deviation traces to the EbsdLib 2.4.1 release commit `5c8c993` (BlueQuartz Software, 2026-05-29), which replaces a precision-fragile `acos(w)` form in `CubicOps::calculateMisorientationInternal` with a numerically-stable `2·atan2(|v|, w)` form using the explicit reduced-quaternion `v` components. The precision improvement is real and mathematically more correct; for `ComputeKernelAvgMisorientationsFilter` specifically it manifests *more strongly than for the per-pair misorientation filters in this cycle* because the kernel inclusion of the focal voxel triggers a per-cell self-misorientation call. For the pre-fix `acos(w)`-form: - -- `q_self_miso = q_focal * q_focal.conjugate() = (0, 0, 0, 1)` mathematically (identity quaternion), so the *true* self-misorientation is exactly 0°. -- The error is **not** introduced by the raw `q * q.conjugate()` product (its `w` component is `|q|² ≈ 1`); it is introduced by the **symmetry reduction**. `calculateMisorientationInternal` maximizes `wmin` over the 24 cubic sym-op candidates, evaluating three candidate forms per sym op: `qco.w()`, `(qco.z() + qco.w())/√2` (the 4-fold-about-c form), and `(qco.x()+qco.y()+qco.z()+qco.w())/2`. For the identity misorientation, several candidates equal 1.0 mathematically — but on float32-sourced quaternions a non-trivial candidate such as `(qco.z() + qco.w())/√2` evaluates to `1 − ε` (with `ε ≈ 1.7e-8`) and can be selected as the maximum. **Which focal quaternions trigger this is candidate-dependent: most reduce on the trivial `qco.w() == 1.0` branch and yield exactly 0; only those whose maximizing candidate is a non-trivial sym-op form land at `1 − ε`.** -- `acos(1 − ε)` near 1 is precision-fragile: the derivative of `acos` at 1 is `-1/√(1-x²) → -∞`, so a `1-ULP` error in `wmin` propagates to a `√(2ε)`-scale error in the angle (then doubled by the `2 * acos(wmin)` step). For `ε` of order `1e-8` this puts the spurious self-miso in the `~0.02–0.03°` range — the fix commit message cites `~0.02°`; the value **measured empirically on this branch is `0.0326°`** (see below). The exact constant depends on the winning sym-op candidate and the platform's float32 quantization, so treat the magnitude as order-of-`0.03°`, not a fixed number. -- The post-fix `2 * atan2(|v|, w)` form, using the **explicit** reduced-quaternion vector components, is numerically stable: components like `(qco.z() - qco.w())` evaluate to *exactly* 0 in IEEE-754 when `qco.z() == qco.w()` regardless of upstream float32 truncation, so `|v| = 0` and the result is exactly 0 for every identity self-misorientation. - -The KAM filter is *more sensitive* than `ComputeFeatureNeighborMisorientations` and `BadDataNeighborOrientationCheck` to this precision improvement because: - -1. **Self-misorientation contribution.** The KAM kernel includes the focal cell (via the `j=k=l=0` inner iteration). For each focal cell, the algorithm therefore makes one call to `calculateMisorientation` with `q1 == q2`. With pre-fix EbsdLib this call returns a spurious ~0.03° **for the subset of focal orientations whose symmetry reduction lands on a non-trivial sym-op candidate** (exactly 0 for the rest), which gets added to `totalMisorientation` and shifts that cell's average up by `(spurious_self / numVoxel)`. With fixed EbsdLib the call returns 0° for *every* focal orientation and contributes nothing. This is the *entire* KAM-specific deviation — see point 3. - -2. **Same-feature large-N averaging.** For a cell in the middle of a large grain with kernel `{1,1,1}`, numVoxel = 27 (all same-feature). The cumulative effect of 27 small precision noises averages out somewhat, but the systematic self-miso contribution is always present. - -3. **The deviation is the self-miso term, essentially nothing else.** For *distinct*-orientation pairs the two EbsdLib forms agree to well below `0.0001°` — empirically confirmed: the sibling `ComputeFeatureNeighborMisorientations` data fixtures assert distinct-pair misorientations of `5.0°` and `10.0°` at margin `1e-3` and **pass against both vcpkg `2.4.0` and the fixed EbsdLib**. That filter excludes the focal feature from its neighbor list, so it never makes a `q1 == q2` call and shows no shift. KAM's per-cell shift is therefore attributable *entirely* to the focal-cell self-misorientation term, not to any per-pair precision noise — which is why KAM is the most observable filter in this cycle for the EbsdLib precision fix. - -**Affected users:** Anyone migrating from DREAM3D 6.5.171 to SIMPLNX on cubic-phase EBSD data with this filter, *or* anyone running a SIMPLNX build pinned to EbsdLib `< 2.4.1` (the standard vcpkg build now pins `≥ 2.4.1`, so this affects only builds on an older baseline). The shift is per-cell, systematic in sign (always slightly above the true KAM), and proportional to the inverse of the kernel volume (1 / numVoxel) — but only for focal cells whose orientation triggers the artifact (see *Symptom*); unaffected focal cells shift by 0. The figures below are the **upper bound for an affected focal cell**: for `KernelSize = {1,1,1}` on a grain interior the affected-cell shift is `~0.03°/27 ≈ 0.001°`; for `KernelSize = {0,0,0}` (single-voxel kernel — just the focal cell) it is the full `~0.03°` because the divisor is 1. - -**Recommendation:** **Trust SIMPLNX (EbsdLib 2.4.1+).** The 6.5.171 result was limited by the well-understood `acos(w near 1)` precision pathology amplified by float32-sourced quaternion inputs; SIMPLNX returns the mathematically correct value. The shift is well below typical EBSD measurement resolution and will not materially affect downstream microstructural analyses, but the cumulative effect on KAM-based maps will be visibly smoother in the post-2.4.1 output. Users requiring exact 6.5.171 reproduction can compile against EbsdLib < 2.4.1 (not recommended). - -For the full root-cause walkthrough of the EbsdLib precision improvement, see the precedent characterization in `vv/deviations/BadDataNeighborOrientationCheckFilter.md` §"Non-deviations" → "EbsdLib 2.4.1 CubicOps precision improvement". The characterization there applies equally to this filter, with the additional amplification factor described above. +## Part 1 — Differences from DREAM.3D v6.5.171 -**Empirical confirmation (V&V cycle, branch `topic/vv/ComputeFeatureNeighborMisorientationsFilter`, 2026-06-04):** The Class 1 / Class 4 data fixtures were run on Apple Silicon against both EbsdLib builds, with per-pair `calculateMisorientation` results instrumented: +### ComputeKernelAvgMisorientationsFilter-D1 -- **Distinct-orientation pairs are exact on both builds.** Pairs of `5°`, `10°`, and `15°` apart returned `4.99991°`, `4.99988°`, etc. (`< 0.0002°` from the analytical value) on both vcpkg `2.4.0` and the fixed local EbsdLib. This rules out per-pair precision noise as a contributor and confirms point 3 above. -- **Self-misorientations are 0 for most focal orientations even pre-fix.** `q1 == q2` for the identity and for the `5°`, `10°`, `15°`-about-c focal cells returned *exactly* `0.0°` on vcpkg `2.4.0`. -- **Only the `20°`-about-c focal cells triggered the artifact pre-fix.** Their self-misorientation returned `0.0325663°` on vcpkg `2.4.0` (matching the `~0.033°` derived above), inflating those cells' KAM by `0.0326°/numVoxel`. Example: the 1D x-axis gradient fixture's last cell (`numVoxel = 2`) read `2.51628°` against an analytical `2.5°` — exceeding the test's `1e-3` margin. -- **The fixed EbsdLib zeroes every self-misorientation.** Rebuilding the same fixtures with the `NX-Com-Qt69-Vtk95-Rel-EbsdLib` preset (local EbsdLib at `5c8c993`), all three misorientation suites pass exactly: KAM `134/134` assertions, `ComputeFeatureNeighborMisorientations` `56/56`, `ComputeFeatureReferenceMisorientations` `238/238`. - -The data-fixture unit tests assert the analytical oracle directly (margin `1e-3`, no tolerance for the pre-fix artifact). With EbsdLib pinned `≥ 2.4.1` in `vcpkg.json` this is the correct, regression-sensitive choice: it holds in every supported build and would immediately flag any future regression of the EbsdLib precision fix, rather than silently absorbing it under a loose tolerance. +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeKernelAvgMisorientationsFilter-D1` | +| **Filter UUID** | `61cfc9c1-aa0e-452b-b9ef-d3b9e6268035` | +| **Status** | active | -**Runtime A/B confirmation on general 3D orientations (V&V cycle, branch `topic/kam_ignore_feature_ids`, 2026-07-15):** the setup described in *Comparison summary* above (identical 12³ / 8-feature / single-cubic-phase input through both `PipelineRunner` 6.5.171 and `nxrunner`, default per-grain path, kernel `{1,1,1}`) produced the following per-cell `KernelAverageMisorientations` deltas over all 1728 cells: +**Symptom:** Cubic kernel averages can differ from DREAM.3D v6.5.171, especially when admitted orientations are identical or nearly identical. -**Input recipe and seed (for reproducibility):** the input was generated by `.superpowers/sdd/task-6-ab/make_input.py` from a single seeded RNG, `np.random.default_rng(1613)` (seed = issue #1613). The 12×12×12 grid is partitioned into 8 features as 2×2×2 octant blocks (single cubic phase, `CrystalStructures = [999, 1]`); each feature is assigned a random-axis base rotation with angle drawn uniformly from 5–25°, and every cell within that feature then receives an independent small intra-grain perturbation (≤3°, about its own random axis) composed onto the feature's base rotation. This is not raw uniform-random unit quaternions across the volume — it is a per-grain-base-plus-scatter construction, chosen so that a spread of focal-cell self-misorientations exercises the D1 precision path. The script is the source of truth for the exact construction. +**Root cause:** Precision. The earlier CubicOps calculation recovers the angle from `acos(w)` near one. Rounding in symmetry reduction can produce a nonzero angle for an identity rotation. The corrected EbsdLib calculation uses explicit reduced-quaternion vector components with `2*atan2(|v|, w)`. SIMPLNX also uses double-precision quaternions where DREAM.3D uses float precision. Both self-pairs and nearly identical distinct pairs can contribute to the difference. -| Metric | Value | -|---|---| -| legacy KAM range (min/mean/max) | 0.952 / 2.142 / 3.551° | -| nx KAM range (min/mean/max) | 0.952 / 2.141 / 3.549° | -| \|Δ\| min / mean / max | 2.4e-7 / 7.5e-4 / **7.2e-3°** | -| cells \|Δ\| > 0.001° | 461 / 1728 | -| cells \|Δ\| > 0.01° | **0** / 1728 | -| signed (legacy − nx): cells legacy>nx / legacy 0`, and shares the focal cell's phase — regardless of whether it belongs to the same feature. There is no way to produce this output with DREAM3D 6.5.171, which only ever computes the per-grain KAM (neighbor must share the focal cell's `featureId`). +### ComputeKernelAvgMisorientationsFilter-D4 -**Root cause:** **Algorithmic choice** — a deliberate feature addition (issue #1613), not a bug, precision effect, or library difference. SIMPLNX adds a second neighbor-inclusion mode; the legacy filter has only the per-grain mode. - -The two modes differ only in the neighbor gate at `Algorithms/ComputeKernelAvgMisorientations.cpp:124`: - -```cpp -const bool neighborContributes = useFeatureIds - ? (featureIds[point] == featureIds[neighborIdx]) // per-grain (legacy-equivalent) - : (featureIds[neighborIdx] > 0 && cellPhases[neighborIdx] == cellPhases[point]); // per-voxel (NX-only, #1613) -``` +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeKernelAvgMisorientationsFilter-D4` | +| **Filter UUID** | `61cfc9c1-aa0e-452b-b9ef-d3b9e6268035` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was affected. | +| **Fixed in** | The DREAM3D-NX release after v7.4.2 | -The focal-validity gate (`featureIds[point] > 0 && cellPhases[point] > 0`), the boundary clamps, the divisor semantics (focal self always included), and the background short-circuit are all identical between the two modes and unchanged from the legacy behavior. +**Symptom:** A positive phase or invalid crystal-structure value on a participating cell can cause an out-of-bounds read. -**Relationship to the default path:** `use_feature_ids = true` is the default and is behavior-identical to every prior SIMPLNX release and to DREAM3D 6.5.171 (up to the D1/D2 precision/bug notes). The per-voxel mode is strictly opt-in; enabling it cannot change the default output. On single-feature single-phase data the two modes are provably equivalent (see the Class 4 mode-equivalence invariant in the V&V report), because every neighbor that passes the per-grain gate also passes the per-voxel gate and vice-versa. +**Root cause:** Bug. Earlier execution reads Crystal Structures through the phase index and then reads the orientation-operator list through the Laue index without range checks. `ValidateKernelAvgMisorientationsPhaseIndices()` now rejects an invalid phase with `-67203` and an invalid Laue index with `-67204`. Both executors call this validator. Cells with nonpositive feature IDs or phases do not participate in validation. -**Affected users:** none in the migration sense — this is additive. Users who want per-voxel KAM (e.g. to visualize sub-grain orientation gradients without feature segmentation, or to include grain-boundary-adjacent lattice curvature) now have it in SIMPLNX with no DREAM3D 6.5.171 equivalent. Users reproducing legacy pipelines leave the parameter at its `true` default and see no change. +**Affected users:** Users whose participating cell phases do not identify ensemble entries, or whose ensemble entries do not identify a supported Laue operator. -**Validation:** Class 1 analytical fixtures (`Class 1 - Per-Voxel Mode`, expected `{5.0, 20/3, 10.0, 10.0, 0, 0}`; `Class 1 - Per-Voxel Mode Two-Phase Gates`, expected `{5.0, 5.0, 0, 0, 0}`) plus the Class 4 `Mode Equivalence on Single Feature` invariant. Because there is no legacy counterpart, this mode is **never** validated by an A/B numeric comparison — the oracle is the sole authority, per V&V policy. +**Recommendation:** Trust SIMPLNX. Correct the reported phase or crystal-structure value before calculating KAM. The fix will be in the DREAM3D-NX release after v7.4.2. -**Recommendation:** **Trust SIMPLNX.** This is a new, oracle-validated capability. No legacy-parity concern applies. +**Evidence:** `Phase and Laue Index Bounds` checks both errors and accepts an invalid phase on background feature zero. All inspected release sources and DREAM.3D v6.5.171 have unchecked lookups; the current common validator performs the checks before calculation. --- -## Non-deviations (algorithm characteristics common to both filters) - -The following behaviors are NOT deviations — SIMPLNX (post-EbsdLib 2.4.1) and DREAM3D 6.5.171 (with D2 dormant on symmetric kernels) agree on them where D1 precision noise is below the user's tolerance. Captured here so future engineers don't re-discover them and propose them as deviations. +### ComputeKernelAvgMisorientationsFilter-D5 -### Focal voxel always included in the kernel sum - -Both implementations have the focal cell as a same-feature neighbor of itself (the `j=k=l=0` inner iteration produces `neighbor = point`). The focal cell's self-misorientation contributes 0° to `totalMisorientation` and 1 to `numVoxel`. This is intentional algorithm characteristic — it provides a non-zero divisor for cells with no in-kernel same-feature neighbors (a single-voxel isolated grain). **Both filters share this behavior** — algorithm characteristic, not a defect. - -### Background cell short-circuit to KAM = 0 +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeKernelAvgMisorientationsFilter-D5` | +| **Filter UUID** | `61cfc9c1-aa0e-452b-b9ef-d3b9e6268035` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was affected. | +| **Fixed in** | The DREAM3D-NX release after v7.4.2 | -Both implementations short-circuit background cells to `KAM = 0` at the *end* of the per-cell processing. Legacy explicitly checks `featureIds[point] == 0 || cellPhases[point] == 0` (line 311). **Since the review-driven cleanup (commit `7f9cddc7d`, 2026-07-16), SIMPLNX expresses the identical condition as an `else` branch** (`.cpp:144-146`) on the preceding `if(featureIds[point] > 0 && cellPhases[point] > 0)` gate (`.cpp:86`) rather than a second explicit `if` (previously at the pre-cleanup line 146) — logically identical by De Morgan's law for these `int32` arrays whose valid domain is non-negative (the only way to fail `> 0` is to equal `0`). For hypothetical out-of-domain negative ids the pre-cleanup code left the output cell holding uninitialized memory (the `DataStore` allocates without a fill value); the `else` now writes a deterministic `0.0f` there — a strict improvement over undefined behavior, not a preserved-behavior change. The logic is AFTER the kernel loop but only fires when the focal validity check at the top failed (so the kernel loop didn't run). **Both filters share this behavior**; the SIMPLNX code shape changed, the observable output did not (confirmed by the unchanged 9-test suite). +**Symptom:** A kernel radius of `INT32_MAX` causes signed overflow when earlier implementations form the inclusive loop bound. -### `numVoxel == 0` fallback (dead code — removed from SIMPLNX 2026-07-16) +**Root cause:** Bug. Earlier loops evaluate a signed radius plus one, such as `kernelSize[0] + 1`. That expression overflows at `INT32_MAX`. Current Direct and Scanline calculations clamp unsigned neighbor coordinates to the image first. The loops stop when each coordinate reaches its clamped maximum. -Legacy `FindKernelAvgMisorientations` still includes an `if(numVoxel == 0) { KAM[point] = 0; }` guard immediately after the `KAM[point] = totalMiso / numVoxel` divide. Prior to the review-driven cleanup, SIMPLNX carried the identical dead-code guard (the former path 9 in the V&V report's code path coverage). In practice the focal voxel always self-matches (former path 6), so `numVoxel >= 1` whenever the focal-validity gate passed — the fallback was provably unreachable in both implementations. **Commit `7f9cddc7d` (2026-07-16) removed the SIMPLNX-side guard as pure dead code** (behavior-preserving — confirmed by the unchanged 9-test suite passing on both in-core and out-of-core builds); legacy retains its copy unchanged. This is now a code-shape difference only, not a behavioral one: both implementations compute the identical KAM for every reachable input. +**Affected users:** Users who supply a maximum signed 32-bit radius through a pipeline or API. -### Multi-threading model +**Recommendation:** Trust SIMPLNX. The fix will be in the DREAM3D-NX release after v7.4.2. In earlier releases, restrict each radius to the extent needed for the image. -Both implementations parallelize over the outer cell loop. Legacy uses `tbb::parallel_for` over a single dimension after marshalling; SIMPLNX uses `ParallelData3DAlgorithm` with a 3D `Range3D`. Both make concurrent reads of the shared input `DataArray`s (FeatureIds, CellPhases, Quats, CrystalStructures). Per the SIMPLNX project policy (`CLAUDE.md`), DataArray subscript access is not formally thread-safe for concurrent reads, but in practice this works for read-only access on contiguous in-memory DataStores. The algorithm has been stable under parallel execution on shipping pipelines; no thread-safety issue surfaced during the V&V cycle's test suite. Out-of-core (OOC) DataStore variants would need explicit testing, but this filter explicitly calls `parallelAlgorithm.requireArraysInMemory(algArrays)` at line 205 (pre-cleanup: line 196), which disables parallelization when any input array is out-of-core (the algorithm then runs serially rather than concurrently accessing the not-thread-safe OOC stores). OOC inputs are still processed correctly — just single-threaded. **Since the review-driven cleanup (commit `7f9cddc7d`), `FindKernelAvgMisorientationsImpl` also builds its `LaueOps` list once in its constructor (a worker member, `.cpp:29,166`) rather than once per `convert()` call — the object is constructed once per `parallelAlgorithm.execute(...)` call and its `m_OrientationOps` member is read-only for the remainder of execution, then shared by const-reference across all parallel `convert()` invocations. This follows the same constructor-built/shared-across-parallel-ranges precedent as `ComputeFeatureFaceMisorientationPerTriangleImpl`, and `calculateMisorientation` remains a const, stateless call on the shared `LaueOps::Pointer` objects — no new thread-safety exposure. Confirmed by the unchanged 9-test suite passing on the out-of-core build (gate 2).** +**Evidence:** `Scanline Handles Maximum Kernel Radius` exercises Direct and Scanline scenarios with all three radii at `INT32_MAX` and requires one zero output value. `Working Set Input Validation` also accepts those radii. All inspected release sources and DREAM.3D v6.5.171 form signed radius-plus-one bounds. diff --git a/src/Plugins/OrientationAnalysis/vv/deviations/ComputeSchmidsFilter.md b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeSchmidsFilter.md index dcbcb246a6..e3449b00a5 100644 --- a/src/Plugins/OrientationAnalysis/vv/deviations/ComputeSchmidsFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeSchmidsFilter.md @@ -1,104 +1,80 @@ -# Deviations from DREAM3D 6.5.171: ComputeSchmidsFilter +# Deviations from DREAM.3D v6.5.171: ComputeSchmidsFilter -This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent. +Part 1 lists differences from DREAM.3D v6.5.171. Part 2 records the separate SIMPLNX output-initialization defect. Both parts use one stable ID sequence; the Filter UUID is the permanent cross-reference anchor. -Entries are referenced by stable ID (`ComputeSchmidsFilter-D`) from the V&V report and from public migration guidance. The ID is stable across renames; the Filter UUID field is the permanent cross-reference anchor. +## Part 1 — Differences from DREAM.3D v6.5.171 ---- - -## ComputeSchmidsFilter-D1 +### ComputeSchmidsFilter-D1 | Field | Value | |---|---| | **Deviation ID** | `ComputeSchmidsFilter-D1` | | **Filter UUID** | `b4681855-0a3d-4237-97f2-5aec509115c4` | | **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | -**Symptom:** Every Schmid factor and both angle components produced by DREAM3D 6.5.171 on the default (auto) slip-system path are systematically too large by a fixed factor. The inflation is uniform, so it is invisible in any relative comparison and only shows up against an absolute reference — or against the physical bound. Concretely, for the maximizing loading direction of a cubic crystal 6.5.171 reports a Schmid factor of **0.500090176**, which is impossible: the Schmid factor cannot exceed 0.5. The reported `cos phi` and `cos lambda` can likewise exceed 1.0. - -The three inflation factors, per output array: - -| Array | 6.5.171 value / correct value | Exact expression | -|---|---|---| -| `SchmidPhis` (`Schmid_Phis`) | 1.0000293384785181 (+0.0029338 %) | `sqrt(3) / 1.732f` | -| `SchmidLambdas` (`Schmid_Lambdas`) | 1.0001510099261918 (+0.0151010 %) | `sqrt(2) / 1.414f` | -| `Schmids` | 1.0001803528351113 (+0.0180353 %) | `sqrt(6) / (1.732f * 1.414f)` | +**Symptom:** Cubic-High automatic Schmid factors were too large by about 0.018%, and the maximum could exceed the physical limit of 0.5. -Measured against 6.5.171 on hand-built fixtures, the observed ratios matched these predictions to better than `1.1e-8` relative. +**Root cause:** Bug in the library. `CubicOps::getSchmidFactorAndSS` divided plane and direction products by `1.732f` and `1.414f`. These values are smaller than `sqrt(3)` and `sqrt(2)`. EbsdLib 3.1.1 replaces them with full-precision constants. The override calculation already used square roots and did not have this defect. -`SlipSystems` and `Poles` are **not** affected. The inflation is a single positive scale factor applied to all twelve candidate slip systems, so the argmax cannot move; and `Poles` is a function of the crystal-frame loading direction, which the normalizers never touch. Both were bit-identical between 6.5.171 and SIMPLNX on every auto-path fixture. +The old-to-correct value ratios are `sqrt(3)/1.732f = 1.0000293384785181` for the plane cosine, `sqrt(2)/1.414f = 1.0001510099261918` for the direction cosine, and `sqrt(6)/(1.732f*1.414f) = 1.0001803528351113` for the factor. The last ratio gives a theoretical maximum of `0.500090176418`. A common positive multiplier does not change the maximizing slip-system index; the normalizers do not enter the pole calculation. -**Root cause:** Library. `OrientationLib/LaueOps/CubicOps.cpp:850-868` in 6.5.171 normalizes the `{111}` plane-normal dot products by the literal `1.732f` and the `<110>` slip-direction dot products by `1.414f`, instead of by `sqrt(3)` and `sqrt(2)`. Both literals are four-significant-digit truncations and both are smaller than the constants they stand in for, so every direction cosine comes out too large. EbsdLib carried the identical defect through version 3.1.0, which means **SIMPLNX built against EbsdLib <= 3.1.0 reproduces this deviation exactly** — this is a shared library defect, not a 6.5.171-only one. +**Affected users:** Users of automatic Cubic-High slip systems in the affected releases. The error changes absolute factors and angle components; it does not apply to every crystal structure. -Fixed in EbsdLib 3.1.1 by replacing both literals with the full-precision `ebsdlib::constants::k_Sqrt3D` and `k_Sqrt2D`. The `double` type matches the surrounding arithmetic, which is entirely `double` — those two `float` literals were the only precision loss in the function. +**Recommendation:** Trust SIMPLNX in DREAM3D-NX v7.4.2 or later. Its full-precision calculation agrees with the analytical expectations in `ComputeSchmidsTest.cpp`. -Note that the *override* slip-system path (`Override Default Slip System` = on) was never affected in either implementation: it normalizes the user-supplied plane and direction with `std::sqrt` and never used the truncated literals. The A/B run confirms this — override-path floats agree between the two builds to `1.2e-7` relative with no systematic bias, which is the control that isolates the normalizer as the sole cause of the auto-path divergence. - -**Affected users:** Everyone who has run Compute Schmid Factors, or legacy Find Schmid Factors, with `Override Default Slip System` off — that is, essentially every user of this filter. Absolute Schmid-factor values are wrong in the fourth decimal place. Relative comparisons *between* Features, rankings, histograms, and the reported slip system are all unaffected, so most downstream analyses reach the same conclusion. - -**Recommendation:** **Trust SIMPLNX**, built against EbsdLib >= 3.1.1. The corrected values match an exact-arithmetic oracle to `1e-6` on all twelve value-asserting Class 1 fixtures and satisfy the physical bound `0 <= m <= 0.5`, which the pre-fix values provably violate. - -No surgical patch to the legacy source was produced for this deviation, deliberately. The defective arithmetic lives in `OrientationLib`, not in `FindSchmids`, so a filter-level patch could not bring the two into alignment and a library-level patch would exceed the "smallest possible diff, one filter per patch" scope of the alignment-validation protocol. The alignment proof used instead is direct: fixed-EbsdLib SIMPLNX reproduces the exact-arithmetic oracle. +**Evidence:** The filter and dependency declarations were read at every `upstream/release/v7.0.0` through `upstream/release/v7.4.2`. Their EbsdLib requirements are 1.0.35, 1.0.37, 1.0.38, 1.0.39, 1.0.40, 2.1.0 and 3.1.2. The corresponding tagged `Source/EbsdLib/LaueOps/CubicOps.cpp` sources retain the literals through 2.1.0; 3.1.1 and 3.1.2 use exact constants. DREAM.3D tag `v6.5.171`, `Source/OrientationLib/LaueOps/CubicOps.cpp`, contains the literals. `vv_work:ComputeSchmidsFilter/predictions.txt` has a post-run amendment dated 2026-08-20, and the NX logs identify a build from that date. The stock legacy runner identifies `SIMPLib Version 1.2.828` in `vv_work:ComputeSchmidsFilter/logs/legacy_auto_123.log`. `vv_work:ComputeSchmidsFilter/results_compare.txt` records 10 pipeline pairs and zero prediction failures, including the expected override-index difference. The largest recorded automatic-path ratio residual is `9.638e-8`; the largest override float difference is `1.788e-7` relative. This comparison predates algorithm change `95768c9a8` (PR #1723, 2026-08-27). Current test source matches CI commit `419f9db35`. All pass in CI at `419f9db35`. --- -## ComputeSchmidsFilter-D2 +### ComputeSchmidsFilter-D2 | Field | Value | |---|---| | **Deviation ID** | `ComputeSchmidsFilter-D2` | | **Filter UUID** | `b4681855-0a3d-4237-97f2-5aec509115c4` | | **Status** | active | +| **Affected released versions** | DREAM.3D v6.5.171 was affected. DREAM3D-NX was not affected. | +| **Fixed in** | DREAM3D-NX v7.0.0 | -**Symptom:** For Features the filter skips entirely — Feature 0, which is the conventional unassigned-Feature sentinel, and any Feature whose crystal-structure value is at or beyond `LaueGroupEnd` — the two angle-component arrays read **`-301.0`** in 6.5.171 and **`0.0`** in SIMPLNX. `Schmids`, `SlipSystems` and `Poles` read `0` in both. - -Valid Laue classes that do not enumerate slip systems are a different path. Their Schmid factor, slip-system index and angle components are zero after the D3 fix, but `Poles` is still computed from the transformed loading direction. +**Symptom:** Skipped angle rows contain `-301` in DREAM.3D v6.5.171 and zero in SIMPLNX. -**Root cause:** Bug in 6.5.171. `FindSchmids.cpp:234` and `:242` create the `SchmidPhis` and `SchmidLambdas` arrays with `createNonPrereqArrayFromPath, AbstractFilter, float>(this, tempPath, -301, cDims)`. The third argument of that template is the array's `initValue`, not an error code; `-301` is an error code that was pasted into the wrong parameter slot. The same call for `Schmids`, `SlipSystems` and `Poles` correctly passes `0`. Because the execute loop writes the angle components only inside `if(xtal < LaueGroupEnd)`, uncomputed rows keep the `-301` sentinel. +**Root cause:** Bug in DREAM.3D. `FindSchmids::dataCheck()` passes `-301` as the initial value for `SchmidPhis` and `SchmidLambdas`. Feature 0 and features with a crystal-structure value at or above `LaueGroupEnd` keep that value because execution does not calculate their angles. The other three output arrays start at zero. SIMPLNX uses zero for skipped rows; its separate explicit-fill correction is `ComputeSchmidsFilter-D7`. -SIMPLNX created all five arrays with `CreateArrayAction`'s default empty fill value, where 6.5.171 passed `initValue 0` for three of them. Preflight now passes an explicit `"0"` fill for all five arrays, and the algorithm additionally writes the Feature-0 row explicitly. +**Affected users:** Users who consume angle values for unassigned or skipped features. A valid class with no automatic slip systems is a separate case: after the D3 fix, its Schmid outputs are zero but its poles are calculated. -**How much exposure that actually carried, measured rather than assumed.** The obvious reading — that without a fill the uncomputed rows hold indeterminate memory — does not hold for the default store. `CoreDataIOManager::addDataStoreFnc()` constructs every `DataStore` with `static_cast(0)` as its `initValue` irrespective of the action's `fillValue` string, and `DataStore`'s constructor unconditionally `std::fill_n`s the buffer with it (`DataStore.hpp:66-69`). Removing the explicit fill and re-running a 20 000-Feature fixture with same-size heap blocks poisoned to `0xAB` and freed immediately before the run still yielded all zeros; a standalone probe confirmed that a same-size raw allocation did return the poison. The zeros were therefore written by SIMPLNX rather than inherited from the allocator. This is a **latent port regression**: the explicit initialization requirement was genuinely dropped even though the tested store masked it. +**Recommendation:** Trust SIMPLNX. Use phase and crystal-structure validity to identify skipped features; do not depend on the undocumented `-301` value. -**Affected users:** Anyone reading Feature 0 or a Feature whose phase maps to an unknown or out-of-range crystal-structure value. Under 6.5.171 those skipped angle rows are a recognisable but undocumented `-301` sentinel. Under SIMPLNX they read `0` — before this cycle by way of the store factory's hard-coded zero rather than by the filter's own intent, and now by both. - -**Recommendation:** **Trust SIMPLNX.** Zero is the meaningful "not computed" value here and is consistent with what the other three output arrays already do for the same rows. Filter uncomputed Features by phase or by `Schmids == 0`, not by looking for `-301`. +**Evidence:** DREAM.3D tag `v6.5.171`, `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/FindSchmids.cpp`, explicitly initializes both angles to `-301`. Released SIMPLNX filter sources never request that value. The v7.0.0 algorithm zeros Feature 0 and its core store initializes skipped rows to zero. `vv_work:ComputeSchmidsFilter/results_compare.txt` records the `-301` versus zero differences for the 2026-08-20 fixture. `options, skip path and phase guards` now asserts every skipped row in a 20,000-feature fixture. --- -## ComputeSchmidsFilter-D3 +### ComputeSchmidsFilter-D3 | Field | Value | |---|---| | **Deviation ID** | `ComputeSchmidsFilter-D3` | | **Filter UUID** | `b4681855-0a3d-4237-97f2-5aec509115c4` | -| **Status** | resolved in EbsdLib 3.1.1 (pinned `>= 3.1.2` in `vcpkg.json`) — fixed in DREAM3D-NX **7.4.2** | - -**Symptom:** Two classes of undefined output from the orientation library's Schmid-factor routines, present in both 6.5.171's `OrientationLib` and EbsdLib through 3.1.0. - -1. `HexagonalLowOps::getSchmidFactorAndSS(load, schmidfactor, angleComps, slipsys)` **read** `schmidfactor` before writing it. The function declares its locals and then makes `if(schmid1 > schmidfactor)` the first statement that touches `schmidfactor`, so the entire comparison chain was driven by an indeterminate value. `slipsys` was likewise never initialized. Depending on the garbage, a Hexagonal-Low Feature could get a Schmid factor of zero with an arbitrary slip-system index, or could accept a candidate against a garbage incumbent. - -2. `HexagonalOps` seeded `schmidfactor` but not `slipsys` or `angleComps`; and the seven Laue classes that enumerate no slip systems (`TrigonalOps`, `TrigonalLowOps`, `TetragonalOps`, `TetragonalLowOps`, `OrthoRhombicOps`, `MonoclinicOps`, `TriclinicOps`) set `schmidfactor` and `slipsys` but left `angleComps` **untouched**. - -Case 2 is the more damaging in practice because it is silent and produces plausible-looking output. A caller that hoists one `angleComps[2]` buffer outside its per-Feature loop — the natural way to write the loop, and what both `FindSchmids` and `ComputeSchmids` did — gets the **previous Feature's** angle components attributed to the current Feature whenever the current Feature's Laue class is one of the seven stubs. The Schmid factor correctly reads 0 while the two angle columns read as real measurements carried over from an unrelated Feature. - -**Root cause:** Bug, in the library. Fixed in EbsdLib 3.1.1 by seeding all four outputs at the top of every affected overload. That changes no value on any path that already computed a result; it only replaces indeterminate or stale output with a defined zero. `CubicOps` and `CubicLowOps` already wrote all four unconditionally and were left alone, as were all eleven plane/direction overloads, which already zeroed their outputs on entry. +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | -Independently of the library fix, SIMPLNX now declares `schmid`, `angleComps` and `slipSystem` **inside** the per-Feature loop, so the stale-carryover half of this defect cannot occur regardless of which EbsdLib the filter is linked against. +**Symptom:** Automatic Schmid calculations could reuse a previous feature's values when a library routine did not initialize every output. -One consequence of the fix, and three observations deliberately **not** changed, are recorded here so the decisions are on the record. +**Root cause:** Bug in the library. `HexagonalLowOps` compared candidates with the incoming factor before resetting it. `HexagonalOps` did not initialize the index or angle components. The seven classes with no automatic slip systems left the angles unchanged. Both filters reused local output variables across features. EbsdLib 3.1.1 initializes these outputs; SIMPLNX also resets its local values inside the feature loop. -**A new `slipsys = 0` "no candidate" sentinel for the hexagonal classes.** `HexagonalOps` and `HexagonalLowOps` assign `slipsys` values **1 through 6** when a candidate wins the strict `>`. Seeding `slipsys = 0` therefore introduces a value *outside* that documented range, and it is now the reportable signal that no candidate ever beat the seeded `schmidfactor = 0.0` — the hexagonal analogue of the override path's `slipsys = 0`. Callers reading a hexagonal `SlipSystems` value must treat `0` as "no slip system found", not as a slip-system number. Before the fix the same situation produced whatever the caller had passed in. Recorded in `docs/ComputeSchmidsFilter.md`. +The hexagonal routines use indices 1 through 6 when a candidate wins. Zero means that no candidate exceeded the initial factor of zero. Cubic-High automatic indices start at zero. These numbering conventions remain different. -**Slip-system numbering base.** `HexagonalOps` and `HexagonalLowOps` number their reported slip systems from **1**, while `CubicOps` numbers from **0**. Changing either would break existing user data. Documented, unfixed. +**Affected users:** Users with hexagonal phases or mixed data in which a feature with calculated angles precedes a Triclinic, Monoclinic, Orthorhombic, Tetragonal or Trigonal feature. -**Affected users:** Anyone whose data contains a Hexagonal-Low phase (defect 1), and anyone whose data mixes a Cubic or Hexagonal phase with any of Triclinic, Monoclinic, Orthorhombic, Tetragonal-Low/High or Trigonal-Low/High while `Store Angle Components of Schmid Factor` is on (defect 2). +**Recommendation:** Trust SIMPLNX in DREAM3D-NX v7.4.2 or later. Treat a zero hexagonal index as no selected slip system, not as an index into its 1-based list. -**Recommendation:** **Trust SIMPLNX** built against EbsdLib >= 3.1.1. For SIMPLNX built against EbsdLib <= 3.1.0, the per-iteration reinitialization added in this cycle already removes the stale-carryover exposure; the `HexagonalLowOps` uninitialized read requires the library fix. +**Evidence:** Every release through `upstream/release/v7.4.1` keeps output locals outside the feature loop in `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeSchmids.cpp`; v7.4.2 resets them inside it. The EbsdLib versions listed in D1 have incomplete initialization before 3.1.1. The corresponding DREAM.3D `v6.5.171` library routines have the same omissions. The current `options, skip path and phase guards` test checks a Triclinic feature after a Cubic-High feature. It does not directly test the hexagonal routines. --- -## ComputeSchmidsFilter-D4 +### ComputeSchmidsFilter-D4 | Field | Value | |---|---| @@ -106,23 +82,21 @@ One consequence of the fix, and three observations deliberately **not** changed, | **Filter UUID** | `b4681855-0a3d-4237-97f2-5aec509115c4` | | **Status** | active | -**Symptom:** Three interface-level differences that are stable across both implementations but easy to trip over. +**Symptom:** Default angle-array names and the error for a slip direction outside its plane differ between DREAM.3D v6.5.171 and SIMPLNX. -1. **The angle-component arrays change units with a boolean toggle.** With `Override Default Slip System` **off**, `Phis` and `Lambdas` hold the direction **cosines** `cos phi` and `cos lambda` — dimensionless, in `[0, 1]`. With it **on**, the same two arrays hold the **angles** `phi` and `lambda` themselves, in **radians**. The two code paths call different overloads: the auto path stores the raw dot products, the override path stores `std::acos` of them. Present identically in 6.5.171 and SIMPLNX. Consequence: with the toggle off the Schmid factor equals the product of the two stored values; with it on it equals the product of their cosines. SIMPLNX parameter descriptions now state these mode-dependent units explicitly. +**Root cause:** Algorithmic choice during the port. DREAM.3D defaults to `SchmidPhis` and `SchmidLambdas`; SIMPLNX defaults to `Schmid_Phis` and `Schmid_Lambdas`. The perpendicularity error changes from `-1001` to `-13500`. SIMPLNX now includes the two vectors and their dot product in the error. -2. **The default angle-array names differ between versions.** 6.5.171 defaults to `SchmidPhis` and `SchmidLambdas`; SIMPLNX defaults to `Schmid_Phis` and `Schmid_Lambdas`. Pipelines converted from SIMPL carry their explicit names across correctly, so this bites only when comparing outputs or hand-writing a downstream path. +Both applications store cosines in automatic mode and radians in override mode. This unit change is shared behavior, not a legacy difference. In automatic mode, `m = Phis * Lambdas`. For a positive override maximum, `m = cos(Phis) * cos(Lambdas)`. When no override candidate exceeds zero, the factor and both angles remain zero; the cosine-product identity does not apply to that sentinel. -3. **The preflight rejection differs.** A slip direction that does not lie in the slip plane is rejected at preflight by both. 6.5.171 reports `-1001` with `"Slip Plane and Slip Direction must be normal"`; SIMPLNX reports `-13500` and includes both vectors and their actual dot product so the user can correct the input. Verified on a dedicated negative fixture. +**Affected users:** Users who construct output paths, match error codes, or process angle arrays after changing the override setting. -**Root cause:** Algorithmic choice / order of operations, for (1); naming and error-code renumbering during the port for (2) and (3). +**Recommendation:** Either interface is acceptable. Use the actual output names and error codes, and interpret angle components according to the selected mode. -**Affected users:** For (1), anyone who toggles `Override Default Slip System` and consumes `Phis`/`Lambdas` — the values silently change units and any downstream arithmetic that assumed cosines is off by a `cos`. For (2) and (3), anyone diffing outputs across versions or matching on error codes. - -**Recommendation:** **Either acceptable within tolerance** — the behaviour is the same in both versions and nothing is numerically wrong. The unit flip is documented in `docs/ComputeSchmidsFilter.md` with an explicit table, and unit tests assert both relationships (`m == Phis * Lambdas` on the auto path, `m == cos(Phis) * cos(Lambdas)` on the override path) so the distinction cannot silently regress. Convert explicitly rather than assuming a unit. +**Evidence:** `FindSchmids.cpp` at DREAM.3D tag `v6.5.171` and current `ComputeSchmidsFilter.cpp` define these defaults and errors. `ComputeSchmidsTest.cpp` checks the positive-factor identities, the zero override sentinel and error `-13500`. `vv_work:ComputeSchmidsFilter/results_compare.txt` and the `legacy_override_badnormal.log`/`nx_override_badnormal.log` files in its `logs/` directory record the comparison. --- -## ComputeSchmidsFilter-D5 +### ComputeSchmidsFilter-D5 | Field | Value | |---|---| @@ -130,39 +104,58 @@ One consequence of the fix, and three observations deliberately **not** changed, | **Filter UUID** | `b4681855-0a3d-4237-97f2-5aec509115c4` | | **Status** | active | -**Symptom:** With `Override Default Slip System` **on**, the `SlipSystems` output disagrees between the two versions for the same input and the same physical answer. On the `(001)[100]` system under `[1,2,3]` loading, 6.5.171 reports **8** and SIMPLNX reports **3**, while the Schmid factor (`3/7` exactly), `Phis`, `Lambdas` and `Poles` all agree. +**Symptom:** With the `(001)[100]` override system and `[1,2,3]` loading, DREAM.3D v6.5.171 reports index 8 and SIMPLNX reports index 3 for the same maximum factor, `3/7`. + +**Root cause:** Algorithmic choice. The override index identifies an entry in the library's symmetry-operator table. The tables contain the same 24 cubic operators but differ at 18 positions. Six operators tie at the maximum. A strict `>` comparison selects the first one in each table, so the indices differ without changing the physical maximum. -This deviation was **not predicted** before the comparison ran. The pre-run prediction asserted `SlipSystems` would be identical on every fixture, reasoning that a uniform positive scale factor cannot move an argmax. That reasoning is correct and does hold on all five auto-path fixtures — the divergence has a different cause entirely, found by adjudicating the unexpected difference rather than by explaining it away. +**Affected users:** Users who compare, store or select override `SlipSystems` values across application versions. Automatic Cubic-High indices instead identify the built-in slip systems. -**Root cause:** Algorithmic choice, specifically an indexing convention. On the override path the reported value is not a slip-system number at all: it is the index of the **symmetry operator**, within that library's 24-element cubic symmetry table, that maps the user-supplied plane and direction onto the maximizing variant. Two things follow. +**Recommendation:** Either result is acceptable. Interpret an override index with the library that produced it; do not treat it as a portable physical label or a unique maximizing operator. -- The two tables hold the same group in a **different order**. `OrientationLib`'s `CubicMatSym` and EbsdLib's `CubicHigh::k_MatSym` differ at **18 of their 24 positions**, but as *sets* they are identical: every one of EbsdLib's 24 matrices occurs exactly once in `OrientationLib`'s table, so there is a well-defined reordering permutation between them. -- **Six operators tie at the maximum, and they are the same six group elements in both libraries.** EbsdLib's tie set `{3, 6, 8, 9, 11, 14}` maps element-by-element through that permutation exactly onto `OrientationLib`'s `{8, 9, 10, 12, 14, 16}` — the pairing is `3->9`, `6->8`, `8->16`, `9->12`, `11->10`, `14->14`. Both implementations are therefore choosing among the *same* six maximizing operators, and each reports whichever member its own enumeration order reaches first under the strict `>`: EbsdLib reaches its index 3 first, `OrientationLib` reaches its index 8 first, and `OrientationLib`'s 8 *is* EbsdLib's 6. That is the whole mechanism — one tie set, two enumeration orders. Verified directly against both source tables in exact rational arithmetic. +**Evidence:** `vv_work:ComputeSchmidsFilter/symop_order.txt` gives the SIMPLNX tie set `{3,6,8,9,11,14}` and the legacy tie set `{8,9,10,12,14,16}`. Its companion `symop_order.py` reads the source tables. `vv_work:ComputeSchmidsFilter/results_compare.txt` records the index difference and matching physical factors within float precision. `override slip system path` pins index 3 for the current table order. + +--- + +### ComputeSchmidsFilter-D6 + +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeSchmidsFilter-D6` | +| **Filter UUID** | `b4681855-0a3d-4237-97f2-5aec509115c4` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | -The two *reported* matrices happen also to be transposes of one another, but that is a coincidence of which member each order reaches first and carries no weight here: transposition does not preserve the Schmid factor when the slip plane and the slip direction differ, so it could not by itself explain the equal maximum. +**Symptom:** Invalid phase indices, unequal feature tuple counts, empty arrays and zero input vectors could cause invalid reads or output. -Neither answer is wrong. The value is a handle into a specific library's table, not a portable physical label, and it is arbitrary among equivalent maximizing operators. +**Root cause:** Bug. Both implementations used unchecked phase indices and normalized vectors without a magnitude check. DREAM.3D collected feature-array paths but did not validate their tuple counts. SIMPLNX now returns `-13501` or `-13502` for an out-of-range or negative phase. Preflight returns `-13503` through `-13508` for unequal feature tuple counts, empty feature arrays, empty Crystal Structures, and invalid loading, plane or direction vectors. The vector guards reject magnitudes below float32 epsilon. -This is distinct from the auto path, where `SlipSystems` **is** a genuine slip-system index into the crystal structure's built-in list (0-11 for Cubic-High) and **was** bit-identical between the two versions on every fixture. +**Affected users:** Pipelines with malformed feature/ensemble selections or zero or very small direction vectors. Valid inputs keep the same calculation. -**Affected users:** Anyone who uses `Override Default Slip System` and records, filters on, or compares the `SlipSystems` value — particularly anyone migrating stored results or scripts from DREAM3D 6.5.x. +**Recommendation:** Trust SIMPLNX in DREAM3D-NX v7.4.2 or later. Supply equal nonzero feature tuple counts, at least one ensemble tuple, valid phase indices and direction vectors with sufficient magnitude. -**Recommendation:** **Either acceptable** — both values identify a correct maximizing symmetry operator. Where a choice is needed, **trust SIMPLNX**, because the SIMPLNX index is the one addressable through EbsdLib's own `getMatSymOp()` accessor, so a user can retrieve the actual matrix the index refers to. Do not treat this value as portable across versions, and do not treat it as uniquely determined even within one version. `docs/ComputeSchmidsFilter.md` now states both caveats, and the unit test that asserts index 3 carries a comment recording that it pins EbsdLib's table order rather than a physical label. +**Evidence:** All release sources through `upstream/release/v7.4.1` omit these guards. `upstream/release/v7.4.2` contains the current filter and algorithm source. DREAM.3D tag `v6.5.171`, `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/FindSchmids.cpp`, also omits the checks. `preflight input validation` asserts six error codes; `options, skip path and phase guards` asserts both phase errors. `vv_work:ComputeSchmidsFilter/mutation_transcript.txt` shows that removing the phase guards caused the guard test to fail. --- -## ComputeSchmidsFilter-D6 +## Part 2 — SIMPLNX defects and behavior changes + +### ComputeSchmidsFilter-D7 | Field | Value | |---|---| -| **Deviation ID** | `ComputeSchmidsFilter-D6` | +| **Deviation ID** | `ComputeSchmidsFilter-D7` | | **Filter UUID** | `b4681855-0a3d-4237-97f2-5aec509115c4` | | **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1 omitted the explicit fill. DREAM.3D v6.5.171 was not affected by this omission. | +| **Fixed in** | DREAM3D-NX v7.4.2 | + +**Symptom:** Skipped output rows depended on the data store's initialization because the filter did not specify their fill value. The tested core store supplied zeros. -**Symptom:** SIMPLNX now rejects malformed inputs before allocating or normalizing data: unequal tuple counts between `Feature Phases` and `Average Quaternions`, empty Feature or Ensemble arrays, a zero loading direction, and zero override slip-plane or slip-direction vectors. DREAM3D 6.5.171 accepts the zero-vector cases. In the independent A/B run, a zero loading direction produced `NaN` Schmid factors and angle components in 6.5.171, while zero override vectors silently produced all-zero Schmid outputs. Before this guard was added, SIMPLNX behaved the same way. A mismatched pair of Feature arrays was more severe in SIMPLNX: the algorithm indexed past both the shorter input and shorter output arrays, corrupting memory and causing `nxrunner` to exit with signal status 139 while writing the result. +**Root cause:** Bug in the output-creation contract. The five `CreateArrayAction` calls omitted a fill value. The algorithm does not write features whose crystal-structure value is at or above `LaueGroupEnd`. DREAM.3D explicitly initialized each array, although its angle value was the separate D2 defect. SIMPLNX now passes `"0"` to all five output actions. -**Root cause:** Bug shared with legacy, plus an added SIMPLNX validation. Both implementations normalized vectors without first checking their magnitude and assumed the two Feature arrays had identical tuple counts. SIMPLNX preflight now returns `-13503` through `-13508` with the offending paths, tuple counts, or vector components. The parameter version is 2 because the parameter descriptions now state the non-zero-vector constraints and mode-dependent output semantics. +**Affected users:** Users with skipped features and a store whose default initialization does not supply zero. This correction does not change the observed core-store output. -**Affected users:** Pipelines with malformed Feature-array selections or zero direction vectors. Valid pipelines are unaffected. Invalid pipelines that previously completed can now fail preflight, but the previous outputs were undefined, non-finite, or the result of memory corruption. +**Recommendation:** Trust SIMPLNX in DREAM3D-NX v7.4.2 or later. Skipped-row values now follow an explicit filter requirement. -**Recommendation:** **Trust SIMPLNX.** Correct the selected array paths so `Feature Phases` and `Average Quaternions` have equal non-zero tuple counts, provide at least one Crystal Structures tuple, and use non-zero direction vectors. Do not consume the legacy zero-vector outputs. +**Evidence:** The output actions omit a fill value in each `upstream/release/v7.0.0` through `upstream/release/v7.4.1` filter source. The v7.4.2 source contains all five explicit fills from PR #1723. The v7.4.1 `src/simplnx/DataStructure/IO/Generic/CoreDataIOManager.cpp` supplies zero to the numeric stores. In `vv_work:ComputeSchmidsFilter/mutation_transcript.txt`, removing explicit fills is the surviving mutation; transpose, quaternion-component swap, pole rounding, tie-break and phase-guard mutations fail their mapped tests. The transcript records five detected mutations out of six. The current 20,000-feature fixture checks skipped rows in all five arrays. diff --git a/src/Plugins/OrientationAnalysis/vv/deviations/ConvertOrientationsFilter.md b/src/Plugins/OrientationAnalysis/vv/deviations/ConvertOrientationsFilter.md index edb2a1b4f3..4321cb7d36 100644 --- a/src/Plugins/OrientationAnalysis/vv/deviations/ConvertOrientationsFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/deviations/ConvertOrientationsFilter.md @@ -1,99 +1,109 @@ -# Deviations from DREAM3D 6.5.171: ConvertOrientationsFilter +# Deviations from DREAM.3D v6.5.171: ConvertOrientationsFilter -This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent (`ConvertOrientations`, SIMPL UUID `e5629880-98c4-5656-82b8-c9fe2b9744de`). +Part 1 lists the differences from DREAM.3D v6.5.171. D1 is retired; D2 through D5 are active. The shared ID sequence is `ConvertOrientationsFilter-D`, and the Filter UUID is the permanent cross-reference anchor. -Entries are referenced by stable ID (`ConvertOrientationsFilter-D`) from the V&V report and from public migration guidance. The ID is stable across renames; the Filter UUID field is the permanent cross-reference anchor. - -The SIMPLNX algorithm is a **Rewrite** of the filter plumbing under the retained UUID (see `../ConvertOrientationsFilter.md`). All four deviations below are consequences of that rewrite. SIMPLNX is verified-correct independently of 6.5.171 against the Class 3 (Rowenhorst 2015) / Class 1 / Class 4 oracle encoded in `test/ConvertOrientationsTest.cpp` (1032 assertions); these entries are diff-explanation, not correctness findings. - -> **Comparison status:** D1, D2, D4 are derived from a side-by-side reading of both implementations (`DREAM3D/.../OrientationAnalysisFilters/ConvertOrientations.cpp` @ 6.5.171 vs `Algorithms/ConvertOrientations.cpp`). A live A/B run is tracked in the report's `Legacy comparison` row. +The filter dispatch is a rewrite. The shared representations retain their conversion roles; D2 through D5 define the input, capability, validation, and Euler-normalization differences. The test source checks all 56 cross-type pairs against EbsdLib reference values and independently checks the stereographic formula. --- -## ConvertOrientationsFilter-D1 +## Part 1 — Differences from DREAM.3D v6.5.171 + +### ConvertOrientationsFilter-D1 | Field | Value | |---|---| | **Deviation ID** | `ConvertOrientationsFilter-D1` | -| **Filter UUID** | `501e54e6-a66f-4eeb-ae37-00e649c00d4b` (SIMPL `e5629880-98c4-5656-82b8-c9fe2b9744de`) | -| **Status** | active | +| **Filter UUID** | `501e54e6-a66f-4eeb-ae37-00e649c00d4b` | +| **Status** | retired 2026-09-17 | + +**Symptom:** The claimed maximum difference of `1.78e-6` did not compare against DREAM.3D v6.5.171. The legacy output came from a local proof version. -**Symptom:** For a given (input, output) representation pair, SIMPLNX and 6.5.171 may differ in the last 1–2 significant figures of float32 output. **Measured** on the toy orientation (Euler 45°/30°/60°): eu→Quaternion, eu→Axis-Angle, eu→Rodrigues, eu→Homochoric are **bit-identical**; eu→OrientationMatrix differs by ≤ 1.5e-8; eu→Cubochoric by ≤ 1.8e-6 (worst single component). Overall max |Δ| = **1.78e-6**. *Scope: the A/B measured only the six Euler-input pairs on a single in-range orientation; the other 36 legacy-shared pairs were compared by source reading only, so the numeric bound above is established for eu→X and inferred (not measured) elsewhere.* +**Root cause:** Library. The comparison used the wrong legacy application. The official DREAM.3D v6.5.171 comparison on 2026-09-17 gives identical components for all six shared Euler-input conversions on the original 45°/30°/60° tuple and on 1,288 interior tuples. Both DREAM3D-NX v7.4.1 and v7.4.2 match. These results do not establish a library precision difference. -**Root cause:** *library-generation drift (OrientationLib vs EbsdLib 3.x).* Both implementations dispatch each requested pair to a **direct** pairwise transform — 6.5.171 `OrientationConverter::convertRepresentationTo()` calls per-pair `toX()` methods that invoke e.g. `eu2om` (the Euler closed form, `OC_CONVERT_BODY(9, OrientationMatrix, eu2om, Eu2Om)`, `OrientationConverter.hpp:492`) and `eu2cu` (which chains `eu2ho→ho2cu`, `:517`); SIMPLNX calls the EbsdLib `input.toX()` members which take the same nominal routes. The residual deltas come from implementation differences accumulated between legacy **OrientationLib** and **EbsdLib 3.x** (constant definitions, expression ordering, series-evaluation details) — largest for Cubochoric, whose cube-root + series expansion is most sensitive to intermediate float32 round-off. That four of six conversions are bit-identical is consistent with this: those transforms' code paths are unchanged between library generations. Both implement the same Rowenhorst 2015 equations; neither is "more correct." *(An earlier draft of this entry attributed the deltas to legacy routing everything through a quaternion intermediate; a source check of `OrientationConverter.hpp` disproved that mechanism and it was corrected here.)* See `../comparisons/ConvertOrientationsFilter/results/comparison.md`. +**Affected users:** None for the claimed precision difference on these fixtures. The Euler-boundary defect is documented separately in D5. -**Affected users:** Anyone diffing SIMPLNX output bit-for-bit against archived 6.5.171 output. Differences (≤ ~2e-6) are below visualization and typical downstream-analysis thresholds. +**Recommendation:** Either application gives identical results on the tested interior fixtures. Do not use the withdrawn `1.78e-6` value as a tolerance bound for other orientations or representation pairs. -**Recommendation:** *either acceptable within tolerance ~1e-5.* Trust SIMPLNX; both satisfy the Rowenhorst oracle within float32 precision, and the measured A/B difference is ≤ 1.8e-6. +**Evidence:** `vv_work:ConvertOrientationsFilter/ReadMe.md` identifies the official `PipelineRunner 1.2.828` and withdraws the earlier result. `vv_work:ConvertOrientationsFilter/results/proof_matrix.json` and `vv_work:ConvertOrientationsFilter/results/proof_matrix.md` record zero differing components for all six interior conversions. The algorithm later changed in `4ec21e93f` (2026-09-20, PR #1568) and `a33c370ca` (2026-09-25, PR #1697); the saved run compares the named releases, not the current algorithm. --- -## ConvertOrientationsFilter-D2 +### ConvertOrientationsFilter-D2 | Field | Value | |---|---| | **Deviation ID** | `ConvertOrientationsFilter-D2` | -| **Filter UUID** | `501e54e6-a66f-4eeb-ae37-00e649c00d4b` (SIMPL `e5629880-98c4-5656-82b8-c9fe2b9744de`) | +| **Filter UUID** | `501e54e6-a66f-4eeb-ae37-00e649c00d4b` | | **Status** | active | -**Symptom:** A pipeline that supplied a **`double`** (float64) orientation array to 6.5.171 cannot supply one to SIMPLNX (the input parameter accepts float32 only), and the output dtype is float32 rather than float64. +**Symptom:** SIMPLNX rejects float64 orientation arrays that DREAM.3D v6.5.171 accepts. -**Root cause:** *precision (deliberate scope reduction).* 6.5.171 `ConvertOrientations::execute()` branched on the input array type and ran the conversion in `double` for `DoubleArrayType` inputs (`generateRepresentation`). SIMPLNX restricts the input `ArraySelectionParameter` to `DataType::float32` and converts in float32 only. For float64 inputs the legacy intermediate math carried ~16 digits vs SIMPLNX's ~7. +**Root cause:** Algorithmic choice. DREAM.3D selects `generateRepresentation` or `generateRepresentation` from the input type. SIMPLNX restricts `ArraySelectionParameter` to `DataType::float32` and converts float32 tuples. -**Affected users:** The small number of legacy pipelines that stored orientations as `double`. Standard EBSD ingest produces float32 orientations, which are unaffected. +**Affected users:** Users whose input orientation arrays use float64. -**Recommendation:** *trust SIMPLNX for float32 workflows.* Users with float64 orientation arrays who require double-precision conversion should note this scope reduction; for EBSD-scale data float32 is the native precision and the difference is immaterial. +**Recommendation:** Trust SIMPLNX for float32 input. Convert float64 input to float32 only when that precision meets the application requirements; DREAM.3D v6.5.171 retains float64 conversion support. + +**Evidence:** The DREAM.3D v6.5.171 source `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/ConvertOrientations.cpp` contains both conversion types. `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ConvertOrientationsFilter.cpp` accepts only float32. Every inspected DREAM3D-NX release from v7.0.0 through v7.4.2 has that restriction. --- -## ConvertOrientationsFilter-D3 +### ConvertOrientationsFilter-D3 | Field | Value | |---|---| | **Deviation ID** | `ConvertOrientationsFilter-D3` | -| **Filter UUID** | `501e54e6-a66f-4eeb-ae37-00e649c00d4b` (SIMPL `e5629880-98c4-5656-82b8-c9fe2b9744de`) | +| **Filter UUID** | `501e54e6-a66f-4eeb-ae37-00e649c00d4b` | | **Status** | active | -**Symptom:** SIMPLNX offers a **Stereographic** representation (input/output type index 7) that 6.5.171 does not. +**Symptom:** SIMPLNX offers Stereographic as input and output type 7; DREAM.3D v6.5.171 does not. + +**Root cause:** Algorithmic choice. DREAM.3D creates seven converter classes. SIMPLNX dispatches eight representations, including Stereographic. -**Root cause:** *algorithmic choice (new capability).* 6.5.171 `generateRepresentation` constructed a 7-element converter vector (Euler, OrientationMatrix, Quaternion, AxisAngle, Rodrigues, Homochoric, Cubochoric); Stereographic did not exist. SIMPLNX adds the 8th type, verified analytically (Class 1, `st = (x,y,z)/(1+w)`) in the unit test. +**Affected users:** Users who select Stereographic. DREAM.3D v6.5.171 has no equivalent conversion path. -**Affected users:** None negatively — purely additive. There is no 6.5.171 output to compare against for any conversion involving Stereographic. +**Recommendation:** Trust SIMPLNX. `Stereographic closed form (Class 1)` checks Quaternion-to-Stereographic values from `(qx,qy,qz)/(1+qw)`; the dispatch matrix checks the other Stereographic pairs against EbsdLib reference values. -**Recommendation:** *trust SIMPLNX.* New capability with an independent analytical oracle; no legacy equivalent exists. +**Evidence:** The seven legacy converters are in `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/ConvertOrientations.cpp`. The eight current dispatch groups are in `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ConvertOrientations.cpp`; the checks are in `src/Plugins/OrientationAnalysis/test/ConvertOrientationsTest.cpp`. --- -## ConvertOrientationsFilter-D4 +### ConvertOrientationsFilter-D4 | Field | Value | |---|---| | **Deviation ID** | `ConvertOrientationsFilter-D4` | -| **Filter UUID** | `501e54e6-a66f-4eeb-ae37-00e649c00d4b` (SIMPL `e5629880-98c4-5656-82b8-c9fe2b9744de`) | +| **Filter UUID** | `501e54e6-a66f-4eeb-ae37-00e649c00d4b` | | **Status** | active | -**Symptom:** Selecting the same representation for input and output produces an error in SIMPLNX (`-67005`) before any computation; 6.5.171 errored similarly (`-1000`) but with a different code/message, and 6.5.171 additionally emitted distinct error codes for out-of-range type indices (`-1001`/`-1002`). +**Symptom:** Invalid inputs return different error codes in SIMPLNX and DREAM.3D v6.5.171. -**Root cause:** *algorithmic choice (preflight refactor).* SIMPLNX delegates input/output type-range validation to the `ChoicesParameter` (which emits the framework `k_Validate_OutOfRange_Error`) and keeps only the same-type check (`-67005`) and array-shape checks (`-67003`/`-67004`) in `preflightImpl`. The legacy `-1001`/`-1002`/`-1004` (converter-failure) error codes have no SIMPLNX equivalent. +**Root cause:** Algorithmic choice. SIMPLNX returns `-67005` for equal representation types and `-67004` for an incorrect component count. DREAM.3D uses `-1000` and `-1006`. SIMPLNX delegates out-of-range type indices to `ChoicesParameter`; DREAM.3D uses `-1001` and `-1002` for input and output indices. SIMPLNX also rejects multidimensional component shapes with `-67003`. -**Affected users:** Scripts or tests that matched on the legacy numeric error codes `-1000`/`-1001`/`-1002`. No effect on successful conversions. +**Affected users:** Scripts and tests that match numeric error codes from DREAM.3D v6.5.171. -**Recommendation:** *trust SIMPLNX.* Behavior is equivalent (invalid configurations are still rejected at preflight); only the error-code surface changed. +**Recommendation:** Trust SIMPLNX. Update numeric error checks to the SIMPLNX codes; both applications reject equal types, out-of-range indices, and incorrect component counts. + +**Evidence:** Compare DREAM.3D v6.5.171 `ConvertOrientations::dataCheck()` with current `ConvertOrientationsFilter::preflightImpl()`. `Invalid preflight` checks the index and component-count errors; `Equal Representations` checks all eight equal-type choices. --- -## ConvertOrientationsFilter-D5 +### ConvertOrientationsFilter-D5 | Field | Value | |---|---| | **Deviation ID** | `ConvertOrientationsFilter-D5` | -| **Filter UUID** | `501e54e6-a66f-4eeb-ae37-00e649c00d4b` (SIMPL `e5629880-98c4-5656-82b8-c9fe2b9744de`) | +| **Filter UUID** | `501e54e6-a66f-4eeb-ae37-00e649c00d4b` | | **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.0 changed the converted orientation through Euler normalization. DREAM.3D v6.5.171 was affected and also changed the stored input. | +| **Fixed in** | DREAM3D-NX v7.4.1 | + +**Symptom:** Euler normalization can change the represented rotation for negative angles, out-of-range `Phi`, or float32 `Phi = pi`. DREAM.3D v6.5.171 also overwrites the stored Euler tuple. -**Symptom:** For Euler-angle **input** outside `[0,2π]×[0,π]×[0,2π]`, 6.5.171 and SIMPLNX produce **genuinely different orientations** (not float round-off), and 6.5.171 additionally **mutated the user's stored input array** while SIMPLNX leaves it untouched. +**Root cause:** Bug. DREAM.3D calls `EulerConverter::sanityCheckInputData()` before conversion. It applies modulo `2*pi`, `pi`, and `2*pi`, then changes negative values to positive values. These operations do not preserve every rotation. The float32 value of `pi` exceeds the double modulo constant, so the middle angle can wrap to nearly zero. DREAM3D-NX v7.0.0 through v7.4.0 apply the same normalization to a local tuple, without changing the stored array. DREAM3D-NX v7.4.1 and later omit this normalization and convert the supplied tuple. -**Root cause:** *algorithmic choice (sanitization removed).* Every legacy `toX()` ran `sanityCheckInputData()` (`OC_CONVERT_BODY`, `OrientationConverter.hpp:386,403`); for Euler input, `EulerConverter::sanityCheckInputData()` ran `EulerSanityCheck` (`:425-446`) **in place on the actual stored array**: `fmod(φ1, 2π)`, `fmod(Φ, π)`, `fmod(φ2, 2π)` followed by sign flips for negative values. Two consequences: (a) the persisted `EulerAngles` array was silently modified as a side effect of running the filter; (b) `fmod(Φ, π)` and the sign flips are **not rotation-preserving**, so for out-of-range input the legacy conversion result corresponds to a *different orientation* than the one stored. SIMPLNX copies each tuple into a local `OrientationF` and never normalizes, converting exactly the orientation the user supplied. The A/B comparison (D1) used only in-range angles, so this difference did not appear in the measured deltas. +**Affected users:** Users with these Euler inputs in the affected releases. Changed rotations can alter downstream orientation analysis. On the comparison matrix, DREAM.3D v6.5.171 changed 143 of 1,288 stored tuples at the float32 `Phi = pi` boundary. -**Affected users:** Pipelines feeding Euler angles outside the fundamental Bunge ranges (e.g. negative angles from upstream arithmetic, or Φ ∈ (π, 2π)). Under 6.5.171 those inputs were silently rewritten and converted as a different orientation; under SIMPLNX they convert as-supplied (EbsdLib's transforms are periodic in φ1/φ2, so only Φ out of `[0,π]` yields a mathematically distinct rotation description). +**Recommendation:** Trust SIMPLNX. Use DREAM3D-NX v7.4.1 or later, which preserves the input and omits the lossy normalization. Recalculate outputs produced from affected Euler tuples in earlier releases. -**Recommendation:** *trust SIMPLNX.* Mutating input data as a side effect was a defect-prone behavior, and `fmod(Φ, π)` silently changed the orientation. Users with out-of-range Euler data should normalize it explicitly (upstream) rather than rely on a lossy implicit rewrite. +**Evidence:** The DREAM.3D v6.5.171 source `Source/OrientationLib/OrientationMath/OrientationConverter.hpp` applies `EulerSanityCheck` to the stored input. Source on every `upstream/release/v7.0.0` through `upstream/release/v7.4.0` calls `m_CheckFunc(input.data())` after copying a tuple in `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ConvertOrientationsFilter.cpp`. The v7.4.1 and v7.4.2 algorithm sources call `inputInstance.to##TO_REP()` without that check. The 2026-09-17 artifacts `vv_work:ConvertOrientationsFilter/results/proof_matrix.json`, `vv_work:ConvertOrientationsFilter/results/proof_matrix.md`, and `vv_work:ConvertOrientationsFilter/LegacyPatch.md` isolate the legacy normalization with a local proof version. For `(-0.75, 4.0, 7.0)`, removing the normalization preserves the input and brings all six outputs within `1.2e-7` of DREAM3D-NX v7.4.2. On the boundary matrix, corrected orientation matrices agree within `5.96e-8` and quaternions are identical; raw Rodrigues and Cubochoric representations still differ at the 180-degree singularity. The current filter tests do not include negative-angle or `Phi = pi` regression fixtures. diff --git a/src/Plugins/OrientationAnalysis/vv/deviations/GroupMicroTextureRegionsFilter.md b/src/Plugins/OrientationAnalysis/vv/deviations/GroupMicroTextureRegionsFilter.md index ab0a21c92a..9f1b6d48a8 100644 --- a/src/Plugins/OrientationAnalysis/vv/deviations/GroupMicroTextureRegionsFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/deviations/GroupMicroTextureRegionsFilter.md @@ -1,190 +1,155 @@ -# Deviations from DREAM.3D 6.5.171: GroupMicroTextureRegionsFilter +# Deviations from DREAM.3D v6.5.171: GroupMicroTextureRegionsFilter -This file lists every behavioral difference between the released SIMPLNX filter and its DREAM3D legacy equivalent (`Source/Plugins/Reconstruction/ReconstructionFilters/GroupMicroTextureRegions.{h,cpp}`, which inherits from the `GroupFeatures` base). It describes the **current state of the shipped code**, not the development history of either implementation. +Part 1 lists differences from the private DREAM.3D v6.5.171 filter source. Part 2 lists SIMPLNX defects. DREAM.3D v6.5.171 does not register this filter for pipeline use. -Entries are referenced by stable ID (`GroupMicroTextureRegionsFilter-D`) from the V&V report and from public migration guidance. The ID is stable across renames; the Filter UUID field is the permanent cross-reference anchor. - -**Legacy comparison target.** DREAM.3D 6.5.171 contains this filter in `_PrivateFilters`. The release compiles the source but does not register the filter for GUI or pipeline use. Therefore, a direct pipeline comparison is not possible. The empirical A/B comparison used DREAM.3D 6.6.382, built from commit `107b8d51b`, because that version registers the filter. Both implementations ran only Group MicroTexture Regions on byte-identical input. The independent Class 1 and Class 4 oracle establishes correctness. The DREAM.3D 6.6.382 comparison provides deviation and migration evidence. - ---- - -## GroupMicroTextureRegionsFilter-D1 - -| Field | Value | -|------------------|---------------------------------------------------| -| **Deviation ID** | `GroupMicroTextureRegionsFilter-D1` | -| **Filter UUID** | `3f695987-81b1-47c3-8cff-b49cfa219be0` | -| **Status** | closed — no deviation | - -Reserved ID, retained so cross-references do not dangle. The released SIMPLNX filter matches legacy behaviour for both settings of `UseNonContiguousNeighbors`: the non-contiguous neighbor list is required only when the user opts into it, and the *use* of that list is gated the same way legacy `GroupFeatures::execute()` gates it. No migration impact. +The stable ID `GroupMicroTextureRegionsFilter-D1` remains reserved and closed: no deviation. Both implementations require the non-contiguous neighbor list only when that option is enabled. D1 is not a confirmed defect. --- -## GroupMicroTextureRegionsFilter-D2 +## Part 1 — Differences from DREAM.3D v6.5.171 -| Field | Value | -|------------------|--------------------------------------------------------------| -| **Deviation ID** | `GroupMicroTextureRegionsFilter-D2` | -| **Filter UUID** | `3f695987-81b1-47c3-8cff-b49cfa219be0` | -| **Status** | active — intentional design difference | +### GroupMicroTextureRegionsFilter-D2 -**Deviation:** Parent-ID *labelling* differs. Legacy always randomizes parent IDs, using a clock-derived seed, and offers the user no control over it; consecutive legacy runs on identical input therefore produce different parent-ID values. SIMPLNX exposes a `RandomizeParentIds` parameter that **defaults to `false`**, so by default parent IDs are assigned sequentially in BFS-discovery order and are reproducible run to run. +| Field | Value | +|---|---| +| **Deviation ID** | `GroupMicroTextureRegionsFilter-D2` | +| **Filter UUID** | `3f695987-81b1-47c3-8cff-b49cfa219be0` | +| **Status** | active | -**What is and is not affected:** the *grouping* is identical — the partition of features into regions, the number of regions, and which features share a region all match. Only the integer label attached to each region differs. Output is permutation-equivalent, never bit-identical, unless randomization is disabled on the SIMPLNX side and the comparison is made on the partition rather than on raw values. +**Symptom:** Parent-ID numbers can differ even when the same Features belong to each region. -**SIMPLNX options:** +**Root cause:** Algorithmic choice. DREAM.3D enables final label randomization and creates a new random generator for each seed search. SIMPLNX uses one generator per execution, disables final label randomization by default, and exposes a fixed seed. The final label permutation preserves membership. A change in seed order can change membership when the running average is enabled. -- `RandomizeParentIds=false` (default) → reproducible sequential parent IDs. Suitable for diff testing and exemplar comparison. -- `RandomizeParentIds=true, UseSeed=false` → legacy-like behaviour: shuffled IDs from a nondeterministic seed. Note `UseSeed` now defaults to `true`, so this combination must be selected deliberately. -- `RandomizeParentIds=true, UseSeed=true, SeedValue=` → shuffled IDs that are still reproducible. +**Affected users:** Users who compare raw parent IDs, or use those IDs to assign colors. These are differences from the private DREAM.3D filter source. -**Why it matters downstream:** parent IDs are frequently fed straight into a color map. Sequential IDs mean adjacent regions get adjacent colors, which can make neighbouring regions hard to distinguish; that is precisely what legacy's unconditional shuffle was for. Users who relied on that visual behaviour should set `RandomizeParentIds=true`. +**Recommendation:** Trust SIMPLNX. Compare region membership separately from label numbers. Enable `RandomizeParentIds` for shuffled labels and keep `UseSeed=true` for repeatable output. -**A secondary consequence:** legacy constructs a fresh RNG inside `getSeed()` on every call (`SIMPL_RANDOMNG_NEW()`), whereas SIMPLNX uses one generator seeded once. The seed order does not change the partition when `UseRunningAverage=false`. It can change the partition when `UseRunningAverage=true` because each accepted feature changes the comparison target. See D4 and D6. - -**Recommendation:** Trust SIMPLNX. Compare partitions (equivalence classes), not parent-ID values, when validating against a legacy run. +**Evidence:** `GroupMicroTextureRegions::getSeed()` and `execute()` in the DREAM.3D v6.5.171 source define the legacy randomization. `RandomizeParentIds invariants` checks partition preservation and repeatability in SIMPLNX. The 2026-08-27 comparison is recorded in `vv_work:GroupMicroTextureRegionsFilter/rerun/comparison_results.txt`. --- -## GroupMicroTextureRegionsFilter-D3 - -| Field | Value | -|------------------|--------------------------------------------------------------------------------| -| **Deviation ID** | `GroupMicroTextureRegionsFilter-D3` | -| **Filter UUID** | `3f695987-81b1-47c3-8cff-b49cfa219be0` | -| **Status** | active — bug present in DREAM.3D 6.6.382 | +### GroupMicroTextureRegionsFilter-D3 -**Deviation:** With `UseRunningAverage=true`, legacy validates the crystal structure of only **one** of the two features under consideration; SIMPLNX validates **both**. Consequently, on data containing more than one Laue class, legacy can place a non-hexagonal feature and a hexagonal feature in the same microtexture region. SIMPLNX cannot. +| Field | Value | +|---|---| +| **Deviation ID** | `GroupMicroTextureRegionsFilter-D3` | +| **Filter UUID** | `3f695987-81b1-47c3-8cff-b49cfa219be0` | +| **Status** | active | -**Mechanism.** The acceptance test in `determineGrouping` is: +**Symptom:** With running average enabled, DREAM.3D can group a cubic Feature with a hexagonal Feature. SIMPLNX rejects that pair. -```cpp -if(phase1 == phase2 && (phase1 == EbsdLib::CrystalStructure::Hexagonal_High)) -``` +**Root cause:** Bug. The DREAM.3D reference Laue index starts at zero and is assigned only when running average is disabled. Zero also denotes the hexagonal-high Laue class. The running-average condition therefore checks only the candidate. SIMPLNX reads both Laue indices before the comparison. DREAM.3D v6.5.171 was affected in its private filter source. No DREAM3D-NX release contains this defect; DREAM3D-NX v7.4.2 contains the correction. -In legacy, `phase1` (the reference feature's crystal structure) is assigned only inside the `if(!m_UseRunningAverage)` branch, so with the running average enabled it retains its initial value of `0`. Because `Hexagonal_High` is itself `0`: +**Affected users:** Users of the private DREAM.3D code with a non-hexagonal Feature adjacent to a hexagonal Feature and running average enabled. The defective condition is equivalent to the corrected condition when both Features are hexagonal-high. -```cpp -// EbsdLib EbsdConstants.h (legacy) / EbsdLibConstants.h (modern) -const unsigned int Hexagonal_High = 0; //!< Hexagonal-High 6/mmm -const unsigned int Cubic_High = 1; -``` +**Recommendation:** Trust SIMPLNX. Both Features must be hexagonal-high before their c-axis angle can permit grouping. -the test does not fail — it degenerates from *"are both features hexagonal-high?"* to *"is the candidate feature hexagonal-high?"*: +**Evidence:** The tagged legacy `determineGrouping()` places the reference assignment inside `if(!m_UseRunningAverage)`. The v7.4.2 release source assigns both classes unconditionally. `Class 1 Analytical (Mixed-Laue Rejection)` checks 20 pairs. `vv_work:GroupMicroTextureRegionsFilter/rerun/mutation_d3_red.log` records 14 failed pair assertions after a temporary restoration of the defect. -``` -phase1 == phase2 && phase1 == Hexagonal_High - -> 0 == phase2 && 0 == 0 - -> phase2 == Hexagonal_High -``` - -That is a strictly **weaker** condition than the intended one, so the legacy defect can only ever *add* grouping; it can never prevent grouping that should occur. SIMPLNX assigns both `phase1` and `phase2` unconditionally, restoring the intended two-sided check. +--- -**Observable scope.** None on single-phase hexagonal data — the ordinary MTR case — where the two conditions are equivalent and legacy and SIMPLNX agree exactly. The deviation appears only when a feature whose Laue class is *not* Hexagonal_High touches one that is. Two distinct *phases* that both resolve to Hexagonal_High (e.g. primary alpha and transformed beta) group together in **both** implementations; that is intended behaviour and was confirmed in external review. +### GroupMicroTextureRegionsFilter-D4 -**Analytical discriminator.** The unit test contains 20 isolated touching pairs. Each pair contains one Cubic_High Feature and one Hexagonal_High Feature with aligned c-axes. The correct two-sided check rejects every pair. A temporary restoration of the one-sided condition caused 14 pair assertions to fail. +| Field | Value | +|---|---| +| **Deviation ID** | `GroupMicroTextureRegionsFilter-D4` | +| **Filter UUID** | `3f695987-81b1-47c3-8cff-b49cfa219be0` | +| **Status** | active | -**Confirmed on real two-phase data with DREAM.3D 6.6.382 (`107b8d51b`).** The result was reproduced at scale -on an alpha/beta titanium scan (`915C_50red`, Oxford `.h5oina`, 2,326,310 -cells; Phase 1 `Ti-Hex` → `Hexagonal_High`, Phase 2 `Titanium cubic` → `Cubic_High`). Both -implementations ran on a byte-identical shared input of **1,840,838 features** containing **106,731 -hexagonal/cubic touching pairs**, at `CAxisTolerance = 20°`: +**Symptom:** The default comparison target and tolerance differ: DREAM.3D uses the touching Feature and 1 degree; SIMPLNX uses the running average and 20 degrees. -- Legacy merged **907 hexagonal/cubic pairs** that SIMPLNX refused. SIMPLNX merged none that legacy - refused. -- The effect is **structural, not stochastic**: varying only the RNG seed produces exactly **zero** - cross-Laue disagreements, so these 907 merges cannot be attributed to region-growth order. -- Applying the one-line `phase1` correction to a local build of the legacy source removed **all 907** - merges, leaving zero cross-Laue disagreements in every configuration tested. +**Root cause:** Algorithmic choice. With `UseRunningAverage=false`, each candidate is compared with the current Feature in the neighbor traversal. With `true`, it is compared with the region's volume-weighted c-axis sum. Each accepted Feature changes that sum, so seed order can change the resulting groups. -This is the first observation of D3 on production data, and it accounts for the 884-group difference -between the two as-shipped runs. With `UseRunningAverage=false` the same input yields **identical -partitions** in both implementations (746,877 groups, zero differing pairs), confirming the defect is -confined to the running-average path. The archived comparison evidence contains the full method and controls. +**Affected users:** Users who reconstruct the private DREAM.3D workflow with default parameters. SIMPL pipeline conversion preserves an explicitly stored `UseRunningAverage` value and tolerance. -**Affected users:** Anyone running legacy `UseRunningAverage=true` on a scan that contains a non-hexagonal indexed phase adjacent to the hexagonal phase of interest. Users whose scans contain a single hexagonal phase — or several phases all of which are hexagonal — are unaffected and their historical results are correct. +**Recommendation:** Trust SIMPLNX. Set the comparison mode and tolerance explicitly when migrating a workflow. Keep a fixed seed when the running average is enabled. -**Recommendation:** Trust SIMPLNX. Migrating an existing legacy MTR pipeline should produce the same regions unless the scan contains a genuinely non-hexagonal phase touching the hexagonal one, in which case SIMPLNX will correctly decline merges that legacy made. +**Evidence:** The DREAM.3D v6.5.171 constructor sets `m_UseRunningAverage(false)` and `m_CAxisTolerance(1.0f)`. Current `parameters()` and the v7.4.2 source set `true` and `20.0f`. The conversion test checks the stored values. The 0/9/18-degree analytical fixture checks running-average grouping at 10 degrees. --- -## GroupMicroTextureRegionsFilter-D4 - -| Field | Value | -|------------------|-----------------------------------------------------------| -| **Deviation ID** | `GroupMicroTextureRegionsFilter-D4` | -| **Filter UUID** | `3f695987-81b1-47c3-8cff-b49cfa219be0` | -| **Status** | active — intentional default difference | - -**Deviation:** The two implementations ship different defaults for `UseRunningAverage`. Legacy defaults to `false` (`m_UseRunningAverage(false)`); SIMPLNX defaults to **`true`**, and has since the initial port. Because that flag selects what a candidate feature is compared against, a pipeline run on defaults does not produce the same regions in the two implementations. +### GroupMicroTextureRegionsFilter-D5 -**Why it matters.** The flag chooses the comparison target for each candidate feature: +| Field | Value | +|---|---| +| **Deviation ID** | `GroupMicroTextureRegionsFilter-D5` | +| **Filter UUID** | `3f695987-81b1-47c3-8cff-b49cfa219be0` | +| **Status** | active | -- `false` — the candidate is compared against the feature it touches (the current BFS frontier). Grouping is the transitive closure of the pairwise tolerance test along neighbour chains, so a region can drift arbitrarily far from its seed orientation one step at a time. At a 20° tolerance, features at 0°, 15°, and 30° all merge. -- `true` — the candidate is compared against the region's running volume-weighted average c-axis, which anchors the region and bounds that drift. On the same three features, 0° and 15° merge and 30° does not. +**Symptom:** DREAM.3D creates an `Active` array. SIMPLNX omits this output and retains the parent Attribute Matrix. -Both behaviours are correct and intended; they answer different questions. External review (A. Pilchak, Pratt & Whitney, 2026-08-11) confirmed both should be offered as user-selectable options, that neighbour-to-neighbour is appropriate when enforcing a misorientation requirement between adjacent features, and that the running average is appropriate when the region's ensemble orientation feeds downstream structure–property modelling. The reviewer's own practice is to use the running average. +**Root cause:** Algorithmic choice. The private DREAM.3D source creates the array with true values and uses its tuple count to size the parent output. It does not use those values to decide grouping. SIMPLNX stores the parent count directly. DREAM3D-NX v7.4.2 already omits the array. -**Affected users:** Anyone migrating a legacy pipeline that left "Group C-Axes With Running Average" at its default. Legacy ran the neighbour-to-neighbour path; SIMPLNX will run the running-average path unless the flag is explicitly cleared. Expect fewer, tighter regions in SIMPLNX on the same data — the anchored comparison rejects chains the legacy default would have accepted. +**Affected users:** Users whose migrated workflows explicitly select `Active`. The Feature-parent and cell-parent arrays remain available. -**Reproducibility consequence.** Because the running-average comparison target is order-dependent, its result changes with the RNG seed: on the two-phase dataset above, varying only the seed moved 192,496 of 3,710,475 touching pairs. SIMPLNX therefore defaults `UseSeed` to `true` so the shipped configuration is deterministic. Legacy has no such parameter — its seed is always clock-derived — so legacy runs with the running average enabled are not reproducible run to run. +**Recommendation:** Trust SIMPLNX. Remove references to `Active` and use the parent Attribute Matrix or parent-ID arrays for the required region information. -**Recommendation:** Set `UseRunningAverage` explicitly rather than relying on either default. To reproduce a legacy run exactly, set it to `false`. Note that this deviation is independent of D3: D3 concerns *which Laue classes* are validated when the running average is enabled, whereas D4 concerns *which comparison target* is selected by default. +**Evidence:** The `Pure-Phi Bunge` test checks that `Active` is absent. `SIMPL Backwards Compatibility` checks both conversion fixtures after the obsolete mapping was removed. `vv_work:GroupMicroTextureRegionsFilter/rerun/logs/nx_removed_active_compat_preflight_after_format.log` records warning `-5433` for an obsolete `active_array_name` argument. --- -## GroupMicroTextureRegionsFilter-D5 +### GroupMicroTextureRegionsFilter-D7 -| Field | Value | -|------------------|--------------------------------------------| -| **Deviation ID** | `GroupMicroTextureRegionsFilter-D5` | -| **Filter UUID** | `3f695987-81b1-47c3-8cff-b49cfa219be0` | -| **Status** | active — intentional output removal | +| Field | Value | +|---|---| +| **Deviation ID** | `GroupMicroTextureRegionsFilter-D7` | +| **Filter UUID** | `3f695987-81b1-47c3-8cff-b49cfa219be0` | +| **Status** | active | -**Deviation:** DREAM.3D creates `NewGrainData/Active` and initializes every value to `true`. DREAM3D-NX does not create this array. The parent Attribute Matrix is retained and its tuple count records the number of microtexture regions. +**Symptom:** A candidate at the angular tolerance boundary can be accepted by one implementation and rejected by the other. -**Reason for removal:** `GroupMicroTextureRegionsFilter` never reads an `Active` value. The SIMPLNX port created the array with `false` values and never changed them. No dedicated downstream consumer of this output was identified. The array therefore did not communicate usable state. +**Root cause:** Precision. DREAM.3D v6.5.171 converts degrees using the double-precision `SIMPLib::Constants::k_Pi` constant and stores the result as float. SIMPLNX uses `k_PiF` in the conversion. The two calculations can round to different float32 thresholds. -**Compatibility:** Existing DREAM3D-NX pipelines that contain the removed `active_array_name` argument still load. The pipeline reader reports a warning for the unknown argument. The SIMPL 6.4 and 6.5 conversion fixtures also load successfully because the obsolete legacy `ActiveArrayName` value is ignored. +**Affected users:** Users who compare results with candidate angles at the float32 tolerance boundary. -**Affected users:** A workflow is affected only if it explicitly selects the generated `Active` array. Use the parent Attribute Matrix tuple count or the parent-ID arrays instead. +**Recommendation:** Either result is acceptable within float32 boundary precision. Use the same implementation and explicit tolerance for repeatable comparisons. -**Recommendation:** Trust DREAM3D-NX. Remove references to the retired `Active` array from migrated pipelines. +**Evidence:** The dated SIMPL source defines `k_Pi` as double. The local proof experiment at seed 5489 changed only the tolerance constant and removed two differing touching-pair decisions. Changing vector normalization did not remove them. See `vv_work:GroupMicroTextureRegionsFilter/rerun/pif_hypothesis_results.txt` and `vv_work:GroupMicroTextureRegionsFilter/rerun/floatnorm_hypothesis_results.txt`. --- -## GroupMicroTextureRegionsFilter-D6 +## Part 2 — SIMPLNX defects and behavior changes -| Field | Value | -|------------------|--------------------------------------------| -| **Deviation ID** | `GroupMicroTextureRegionsFilter-D6` | -| **Filter UUID** | `3f695987-81b1-47c3-8cff-b49cfa219be0` | -| **Status** | active — defect fixed in the verified branch | +### GroupMicroTextureRegionsFilter-D6 -**Deviation:** Released DREAM3D-NX versions derived the grouping seed from the system clock by default. With `UseRunningAverage=true`, two runs of the same pipeline could produce different region partitions. +| Field | Value | +|---|---| +| **Deviation ID** | `GroupMicroTextureRegionsFilter-D6` | +| **Filter UUID** | `3f695987-81b1-47c3-8cff-b49cfa219be0` | +| **Status** | active | +| **Affected released versions** | No DREAM3D-NX release. DREAM.3D v6.5.171 was affected in its private filter source when running average was enabled. | +| **Fixed in** | DREAM3D-NX v7.4.2 | -**Evidence:** Two preserved runs differed on 194,658 of 3,710,475 touching pairs. A second fixed-input seed comparison differed on 192,496 pairs. All differences were hexagonal/hexagonal. No cross-Laue differences occurred. +**Symptom:** Before the fixed-seed default was introduced, repeated running-average executions could produce different region partitions. -**Affected users:** DREAM3D-NX v7.0.0 through v7.4.1 users who used the running-average comparison and did not provide a fixed seed. +**Root cause:** Bug. The default seed came from the system clock. The running-average target changes as Features join a region, so a different seed order can change membership. SIMPLNX now enables `UseSeed` by default and stores the seed in the output. -**Resolution:** The verified branch enables `UseSeed` by default. The seed value is stored in a top-level array so the run can be repeated. +**Affected users:** Users of the pre-release implementation who enabled running average without a fixed seed. Released DREAM3D-NX defaults already use a fixed seed. -**Recommendation:** Trust the verified branch. Keep `UseSeed=true` for reproducible grouping. +**Recommendation:** Trust SIMPLNX. Keep `UseSeed=true` and retain the stored seed when repeating an analysis. + +**Evidence:** PR #1703 (`5a0b8327a`) changes the default to true. Direct inspection of all release trees finds no filter in v7.0.0 through v7.4.1; the v7.4.2 source has the correction. `vv_work:GroupMicroTextureRegionsFilter/rerun/preserved_seed_variation_results.txt` records 194,658 differing touching pairs between two runs, and 192,496 in a separate seed comparison. --- -## GroupMicroTextureRegionsFilter-D7 +### GroupMicroTextureRegionsFilter-D8 + +| Field | Value | +|---|---| +| **Deviation ID** | `GroupMicroTextureRegionsFilter-D8` | +| **Filter UUID** | `3f695987-81b1-47c3-8cff-b49cfa219be0` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.4.2. DREAM.3D v6.5.171 was affected in its private filter source. | +| **Fixed in** | The DREAM3D-NX release after v7.4.2 | -| Field | Value | -|------------------|--------------------------------------------| -| **Deviation ID** | `GroupMicroTextureRegionsFilter-D7` | -| **Filter UUID** | `3f695987-81b1-47c3-8cff-b49cfa219be0` | -| **Status** | active — precision difference | +**Symptom:** A participating Feature with a positive phase beyond Crystal Structures can cause an out-of-bounds read. -**Deviation:** The legacy source converts the tolerance from degrees with a double-precision pi constant. DREAM3D-NX uses the float32 pi constant that matches the float32 input and angular calculation. A value at the acceptance boundary can therefore fall on different sides of the comparison. +**Root cause:** Bug. The earlier code used positive Feature phases as Crystal Structures indices without an upper-bound check. SIMPLNX now checks both indices before either lookup and returns `-87001` on failure. An isolated Feature does not require that lookup. -**Evidence:** At seed 5489 and a 20 degree tolerance, the implementations disagreed on two of 3,710,475 touching pairs. Changing only the local legacy tolerance conversion to the float32 pi constant removed both differences. Changing vector-normalization precision did not remove them. At seed 1234, no boundary value produced a difference. +**Affected users:** DREAM3D-NX v7.4.2 users with invalid positive phases on Features considered for grouping. The private DREAM.3D filter source has the same unchecked lookup. -**Affected users:** Only comparisons that contain a candidate at the float32 tolerance boundary. The measured effect was two touching pairs in the production fixture. +**Recommendation:** Trust SIMPLNX. Correct the Feature phase or supply its Crystal Structures entry. The fixed filter returns an error for a participating invalid phase. -**Recommendation:** Either result is acceptable within float32 boundary precision. Use DREAM3D-NX output for current workflows. +**Evidence:** `upstream/release/v7.4.2` reads `m_CrystalStructures.getValue(referenceFeaturePhase)` and the neighbor value without bounds checks. PR #1568 (`4ec21e93f`) adds both checks. `Phase Index Bounds` tests reference, neighbor, and isolated cases. The DREAM.3D `v6.5.171` source also indexes through unchecked phases. + +--- diff --git a/src/Plugins/OrientationAnalysis/vv/deviations/NeighborOrientationCorrelationFilter.md b/src/Plugins/OrientationAnalysis/vv/deviations/NeighborOrientationCorrelationFilter.md index 30f0ee6042..d98ae70996 100644 --- a/src/Plugins/OrientationAnalysis/vv/deviations/NeighborOrientationCorrelationFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/deviations/NeighborOrientationCorrelationFilter.md @@ -1,16 +1,10 @@ -# Deviations from DREAM3D 6.5.171: NeighborOrientationCorrelationFilter +# Deviations from DREAM.3D v6.5.171: NeighborOrientationCorrelationFilter -This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent (`NeighborOrientationCorrelation`, SIMPL UUID `6427cd5e-0ad2-5a24-8847-29f8e0720f4f`). +Part 1 lists differences from DREAM.3D v6.5.171 (D1–D4 and D8). Part 2 lists SIMPLNX test and implementation defects (D5–D7). All entries use one stable ID sequence and the filter UUID as the permanent reference. -Entries are referenced by stable ID (`NeighborOrientationCorrelationFilter-D`) from the V&V report and from public migration guidance. The ID is stable across renames; the Filter UUID field is the permanent cross-reference anchor. +## Part 1 — Differences from DREAM.3D v6.5.171 -**Comparison summary (2026-07-07):** SIMPLNX vs DREAM3D 6.5.171 on the 12 legacy-native V&V fixtures (engineer's V&V working archive, `VV_Work/NeighborOrientationCorrelationFilter/`). 6.5.171 reproduces the diagnostic `legacy` prediction of the reference implementation on all 12; SIMPLNX (post-fix) reproduces the canonical `intended` oracle on all 12. 5 of 12 fixtures show 6.5.171 vs SIMPLNX differences — exactly the fixtures whose outcome depends on a defect (D1: F05; D2: F04, F10; D3: F03, F12) — while the 7 fixtures whose neighborhoods are fully tied produce identical output in both versions because SIMPLNX's argmax resolves ties to the same (last-in-scan-order) neighbor 6.5.171 picked. D4 (precision) is latent by fixture design. Each root cause was proven by applying the corresponding surgical fix to a local build of the legacy 6.5.171 source, after which the legacy output became **bit-identical to SIMPLNX** on all 12 fixtures. - -**Production-scale A/B (2026-07-07):** Small IN100 (4,444,713 cells, 36.6% below the 0.2 confidence threshold, phases {0, 1}), MinConfidence 0.2, tolerance 5 deg, Level 2, identical legacy-native input fed to both runners. SIMPLNX modified 1,115,040 cells (25.09%); 6.5.171 modified 1,022,494 (23.01%). **635,114 cells (14.29% of the volume) differ between the two versions**, decomposed as: 115,380 cells filled only by SIMPLNX (the D2 extra passes reaching deeper into bad regions), 22,834 filled only by 6.5.171 (cascade divergence — different pass-1 sources shift later scan decisions in both directions), and 496,900 of the 999,660 cells modified by both receiving a different replacement (D1/D3 selection differences plus their cascades). The same input through the patched local legacy build reproduced SIMPLNX **bit-for-bit: 0 of 4,444,713 cells differ** — which also bounds D4 (float32 vs float64 misorientation precision) at zero observable effect on this dataset. - ---- - -## NeighborOrientationCorrelationFilter-D1 +### NeighborOrientationCorrelationFilter-D1 | Field | Value | |---|---| @@ -18,17 +12,19 @@ Entries are referenced by stable ID (`NeighborOrientationCorrelationFilter-D` | **Filter UUID** | `4625c192-7e46-4333-a294-67a2eb64cb37` | | **Status** | active | -**Symptom:** 6.5.171 can replace a low-confidence cell with the attributes of a *different-phase* neighbor (fixture `F05_stale_w_mixed_phase`: a phase-1 cell is overwritten with phase-2 data), and more generally can count a mixed-phase or phase-0 neighbor pair as "similar" when it should never be. +**Symptom:** A low-confidence cell can receive a different-phase tuple because an unrelated neighbor pair counted as similar. -**Root cause:** Bug in 6.5.171 (stale `w`). In legacy `NeighborOrientationCorrelation::execute()` the misorientation `w` is a function-scope variable computed only inside the same-phase conditional, but the `w < misorientationToleranceR` similarity test runs unconditionally — a mixed-phase/phase-0 pair inherits whatever `w` the previous pair produced. SIMPLNX re-initializes the axis-angle to `max()` before every pair (`Algorithms/NeighborOrientationCorrelation.cpp`). Same defect class as `BadDataNeighborOrientationCheckFilter-D2`. +**Root cause:** bug. DREAM.3D v6.5.171 stores the misorientation in a function-scope variable. The same-phase condition updates that variable, but the similarity test runs outside the condition. A different-phase or phase-zero pair can reuse an earlier angle. SIMPLNX counts similarities only inside the same-positive-phase condition. -**Affected users:** Anyone running 6.5.171 on multi-phase (or partially-indexed, phase-0-containing) datasets. Cells at phase boundaries can be filled from the wrong phase; single-phase fully-indexed datasets are unaffected. Small IN100 contains phase-0 (unindexed) cells, so D1 contributes to the measured 14.29% production-scale difference (jointly with D3; the fixtures isolate each cause individually). +**Affected users:** Users of DREAM.3D v6.5.171 with mixed-phase or unindexed neighbors. DREAM.3D v6.5.171 was affected. DREAM3D-NX v7.0.0 through v7.4.2 were not affected. -**Recommendation:** Trust SIMPLNX. The 6.5.171 behavior is mathematically incorrect. The root cause was proven by resetting `w` in a local build of the legacy source, which eliminated the difference. +**Recommendation:** Trust SIMPLNX. Only a pair with the same positive phase contributes to the similarity count; the selected pair need not have the center cell's phase. + +**Evidence:** `Oracle F05 - mixed-phase pair never similar (D1 regression)` checks the mixed-phase fixture. The tagged legacy source has the stale-angle path; all ten release sources initialize the pair angle before testing it. See `vv_work:NeighborOrientationCorrelationFilter/notes/ComparisonResults.md` and the source audit in `../provenance/neighbor_orientation_correlation_v2.md`. --- -## NeighborOrientationCorrelationFilter-D2 +### NeighborOrientationCorrelationFilter-D2 | Field | Value | |---|---| @@ -36,17 +32,19 @@ Entries are referenced by stable ID (`NeighborOrientationCorrelationFilter-D` | **Filter UUID** | `4625c192-7e46-4333-a294-67a2eb64cb37` | | **Status** | active | -**Symptom:** 6.5.171 runs only half the intended cleanup passes — `ceil((6 − Level)/2)` instead of the original design's `6 − Level` — so multi-pass fills stop early (fixture `F04_multipass_cascade_L2`, Level 2: a 3×3×3 bad region that needs 3 passes is fully filled by SIMPLNX's 4 passes but left with an unfilled center after legacy's 2 passes). +**Symptom:** DREAM.3D and earlier DREAM3D-NX releases stop cleanup before the intended number of passes. + +**Root cause:** bug. The loop decrements the level both in its header and at the end of its body. It runs `ceil((6 - Level)/2)` passes. SIMPLNX removes the second decrement and runs `6 - Level` passes. Level controls the pass count, not a required number of dissimilar neighbors. -**Root cause:** Bug in 6.5.171 (introduced 2014, DREAM3D commit `0bbeb1d49`). Converting the original 2013 `while(currentLevel > m_Level)` loop to a `for(...; currentLevel--)` loop for progress reporting left the trailing `currentLevel = currentLevel - 1;` in place, decrementing twice per pass. The authoritative statement of intent is the original 2013 implementation, whose single-decrement `while` loop runs exactly `6 − Level` passes (levels 6 down to Level+1). Note that the legacy user documentation's own example ("if the user selects a level of 4, then the filter will run with a level of 6, then 5, then 4") describes `7 − Level` passes and never matched *any* shipped implementation — the doc example was wrong from the filter's birth. SIMPLNX inherited the double decrement at port time; resolved to the original `6 − Level` schedule. +**Affected users:** Users who set Level below 5 and need repeated propagation. DREAM3D-NX v7.0.0 through v7.4.1 were affected. DREAM.3D v6.5.171 was affected. DREAM3D-NX v7.4.2 contains the fix. -**Affected users:** Anyone using *Cleanup Level* ≤ 4 on data where bad regions are more than one erosion layer deep — the filter fills noticeably less than intended. Level 5 datasets (1 pass either way) are unaffected. Measured on Small IN100 (Level 2): the restored passes let SIMPLNX fill 115,380 cells (2.6% of the volume) that 6.5.171 leaves bad. +**Recommendation:** Trust SIMPLNX. The corrected schedule gives four passes at Level 2 and two at Level 4. -**Recommendation:** Trust SIMPLNX (post-fix). It implements the documented and originally-intended pass schedule; applying the same fix to a local build of the legacy source reproduces SIMPLNX's output exactly. +**Evidence:** `Oracle F04 - pass schedule Level 2 (D2 regression)` and `Oracle F10 - pass schedule Level 4 (D2 regression)` check the resulting fill. Direct inspection of all release sources finds the second decrement through v7.4.1 and no second decrement in v7.4.2. PR #1655 (`71526a1f5`) contains the correction. --- -## NeighborOrientationCorrelationFilter-D3 +### NeighborOrientationCorrelationFilter-D3 | Field | Value | |---|---| @@ -54,17 +52,19 @@ Entries are referenced by stable ID (`NeighborOrientationCorrelationFilter-D` | **Filter UUID** | `4625c192-7e46-4333-a294-67a2eb64cb37` | | **Status** | active | -**Symptom:** When a bad cell's neighbors have unequal similarity counts, 6.5.171 copies from the *last* scanned neighbor that has any similar pair, not the *most* similar neighbor (fixture `F03_argmax_vs_lastwins`: a count-1 neighbor beats a count-3 neighbor because it is scanned later). +**Symptom:** A later neighbor with a smaller positive similarity count can replace a neighbor with a larger count. -**Root cause:** Bug in 6.5.171, present since the filter's 2013 introduction. The best-neighbor selection loop resets `best = 0` inside the per-neighbor loop, degrading the intended arg-max (`if(neighborSimCount[j] > best)` with `bestNeighbor` bookkeeping) to "last neighbor with count > 0 wins" — contradicting both the variable's purpose and the documented "replaced with the best neighbor". SIMPLNX inherited the quirk at port time; resolved. The corrected argmax resolves ties to the **last** neighbor in the fixed −Z,−Y,−X,+X,+Y,+Z scan order (`>=` with a count > 0 guard), deliberately chosen so that fully-tied neighborhoods — the common case in grain interiors — pick the *same* neighbor as 6.5.171. Migration differences therefore concentrate exclusively where the ranking defect genuinely changed the outcome. +**Root cause:** bug. DREAM.3D resets `best` inside the selection loop. Any positive count therefore replaces the previous selection. SIMPLNX retains `best` across the loop and selects the greatest positive count. Equal maxima select the last neighbor in −Z, −Y, −X, +X, +Y, +Z order. -**Affected users:** 6.5.171 replacement decisions where the neighbor similarity counts are unequal and the maximum is not the last positive count in scan order — typically at grain boundaries and noise-cluster edges. Fully-tied neighborhoods (grain interiors) are **not** affected: SIMPLNX's last-of-ties argmax picks the identical neighbor there. Measured on Small IN100: 496,900 of the 999,660 cells modified by both versions (49.7%) received a different replacement (jointly with D1 and cascade effects). **This also changes results relative to prior SIMPLNX/DREAM3D-NX releases**, which inherited the legacy quirk at port time: a pipeline re-run after this fix (with `Level < 5`, where a cell can have unequal neighbor counts) can produce different cleanup than the same pipeline produced on an earlier NX build. This should be called out in the release notes for the version that ships this fix, not only as a legacy-migration note. +**Affected users:** Users with unequal neighbor similarity counts. DREAM3D-NX v7.0.0 through v7.4.1 were affected. DREAM.3D v6.5.171 was affected. DREAM3D-NX v7.4.2 contains the fix. -**Recommendation:** Trust SIMPLNX (post-fix). The 6.5.171 selection ignores the computed similarity ranking; applying the same fix to a local build of the legacy source reproduces SIMPLNX's output exactly. +**Recommendation:** Trust SIMPLNX. The selected neighbor has the greatest similarity count; fully tied neighborhoods retain the legacy choice. + +**Evidence:** `Oracle F03 - argmax selection (D3 regression)` and `Oracle F12 - anisotropic dims` distinguish the rules. All release sources through v7.4.1 reset `best` per neighbor. The v7.4.2 source contains the maximum-count rule from PR #1655 (`71526a1f5`). --- -## NeighborOrientationCorrelationFilter-D4 +### NeighborOrientationCorrelationFilter-D4 | Field | Value | |---|---| @@ -72,19 +72,100 @@ Entries are referenced by stable ID (`NeighborOrientationCorrelationFilter-D` | **Filter UUID** | `4625c192-7e46-4333-a294-67a2eb64cb37` | | **Status** | active | -**Symptom:** Latent. A neighbor pair whose misorientation sits within float32 round-off of the tolerance can be classified "similar" by one version and "not similar" by the other, cascading into different replacements. +**Symptom:** A pair close to the angle tolerance can receive a different similarity decision because the orientation calculations use different precision. + +**Root cause:** precision. DREAM.3D v6.5.171 uses `QuatF` and `getMisoQuat`. SIMPLNX converts the stored float32 quaternion components to `ebsdlib::QuatD` and calls `calculateMisorientation`. The angle result is double precision; conversion of the tolerance to radians still uses float32. -**Root cause:** Precision. 6.5.171 computes misorientations with `QuatF`/`getMisoQuat` (float32); SIMPLNX uses `ebsdlib::QuatD`/`calculateMisorientation` (float64). Not observable in the V&V fixtures, which keep every pair ≥ 1° away from the 5° tolerance by design; on real EBSD data isolated boundary-straddling pairs may flip. +**Affected users:** Users whose neighbor-pair angles are close to the selected tolerance. This is a numerical difference, not a confirmed defect. -**Affected users:** Only datasets containing neighbor misorientations within ~10⁻⁵ degrees of the chosen tolerance — rare and physically meaningless differences. Measured bound: on Small IN100 (4.44M cells, ~10⁸ pair comparisons across 4 passes), float64 SIMPLNX and the float32 patched legacy build produced **zero** differing cells. +**Recommendation:** Trust SIMPLNX. Avoid requiring identical classification across implementations for a pair at the tolerance boundary. -**Recommendation:** Either acceptable. Double precision is not materially more correct at the tolerance boundary; differences are confined to knife-edge pairs. +**Evidence:** The legacy and current algorithm sources establish the arithmetic types. The 2026-07-07 comparison found no cell difference between that SIMPLNX version and a local proof version on Small IN100. This measurement does not bound other inputs or the current slice-transfer implementation; see `vv_work:NeighborOrientationCorrelationFilter/notes/ComparisonResults.md`. --- -## Shared characterized behaviors (not deviations — identical in both versions) +### NeighborOrientationCorrelationFilter-D8 + +| Field | Value | +|---|---| +| **Deviation ID** | `NeighborOrientationCorrelationFilter-D8` | +| **Filter UUID** | `4625c192-7e46-4333-a294-67a2eb64cb37` | +| **Status** | active | + +**Symptom:** Replacement chains can copy different tuples from DREAM.3D when source cells are also replaced. + +**Root cause:** order of operations. DREAM.3D copies tuples in ascending index order and retains the source map across passes. Current SIMPLNX copies numeric tuples from the store state at the start of each slice transfer, then clears that slice's source marks. NeighborList and String arrays still use ascending in-place copies. DREAM3D-NX v7.0.0 through v7.4.2 retain the earlier transfer method; the changed method will be in the DREAM3D-NX release after v7.4.2. + +**Affected users:** Users with adjacent low-confidence cells whose selected sources also change. Numeric tuples do not chain within one slice transfer. The selection window and the tuple-transfer buffers serve different purposes. + +**Recommendation:** Use the current slice-transfer rules when interpreting replacement chains. The isolated-cell analytical fixtures check source selection but do not establish whole-volume parity with DREAM.3D. + +**Evidence:** PR #1568 (`4ec21e93f`) replaces the volume source map with `sliceBestNeighbor`, adds `SliceBufferedTransferOneZ`, and clears marks after each slice. `src/simplnx/Utilities/SliceBufferedTransfer.hpp::SliceTransferOneZFunctor` reads source slices before writing the destination. The 2026-07-07 comparison predates this change. + +--- + +## Part 2 — SIMPLNX defects and behavior changes + +### NeighborOrientationCorrelationFilter-D5 + +| Field | Value | +|---|---| +| **Deviation ID** | `NeighborOrientationCorrelationFilter-D5` | +| **Filter UUID** | `4625c192-7e46-4333-a294-67a2eb64cb37` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1 test sources. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | + +**Symptom:** The production test can pass without comparing any filter output with its exemplar. + +**Root cause:** bug. The test substitutes `Exemplar Data` for the container name, but the retired output file uses `DataContainer`. Missing arrays trigger `continue`. The corrected test snapshots the input cell arrays and checks transfer invariants; analytical fixtures independently check exact tuples. + +**Affected users:** Developers relying on the earlier exemplar test as correctness evidence. This is a test defect, not a separate output transformation. + +**Recommendation:** Trust the current analytical and invariant tests. Do not use the retired exemplar comparison as evidence of correctness. + +**Evidence:** All ten release test sources were inspected. Sources through v7.4.1 use the missing-array skip; v7.4.2 uses snapshots and requires at least one changed cell. `vv_work:NeighborOrientationCorrelationFilter/notes/ComparisonResults.md` records the archive container mismatch. PR #1655 (`71526a1f5`) removes the output archive registration. + +--- + +### NeighborOrientationCorrelationFilter-D6 + +| Field | Value | +|---|---| +| **Deviation ID** | `NeighborOrientationCorrelationFilter-D6` | +| **Filter UUID** | `4625c192-7e46-4333-a294-67a2eb64cb37` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | + +**Symptom:** A replaced cell keeps its old NeighborList and String values while its numeric values change. + +**Root cause:** bug. The earlier transfer list collects only `IDataArray` objects. NeighborList and String arrays are separate types. The corrected list collects `IArray` objects and copies each supported type. DREAM.3D collects every array in the cell AttributeMatrix. + +**Affected users:** Users with NeighborList or String arrays beside the confidence array. + +**Recommendation:** Trust SIMPLNX. The transfer includes these arrays, and F13 checks their source tuples and unchanged cells. + +**Evidence:** PR #1655 (`71526a1f5`) adds `GenerateTransferArrayList`, the concrete copy dispatch and `Oracle F13 - NeighborList and String cell arrays transferred`. All release sources through v7.4.1 use `GenerateDataArrayList`; v7.4.2 includes all three types. The tagged DREAM.3D source loops over all cell arrays. + +--- + +### NeighborOrientationCorrelationFilter-D7 + +| Field | Value | +|---|---| +| **Deviation ID** | `NeighborOrientationCorrelationFilter-D7` | +| **Filter UUID** | `4625c192-7e46-4333-a294-67a2eb64cb37` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was affected. | +| **Fixed in** | The DREAM3D-NX release after v7.4.2 | + +**Symptom:** An invalid participating phase or crystal-structure value can cause an out-of-bounds array access. + +**Root cause:** bug. Earlier sources index Crystal Structures through the cell phase and index the orientation operators through that value without range checks. SIMPLNX now checks each participating positive phase before either lookup. It returns -580096 for an invalid phase and -580097 for an invalid Laue index. Nonpositive phases do not participate. + +**Affected users:** Users with a same-positive-phase neighbor pair whose phase exceeds the ensemble bounds or whose crystal structure is not a valid Laue index. + +**Recommendation:** Trust SIMPLNX. Correct the phase or crystal-structure value reported in the error before running cleanup again. -- **All cell array types are transferred**: the per-pass transfer copies numeric DataArrays *and* NeighborList and String cell arrays into replaced cells, so every attribute of a replaced cell stays consistent. Matches 6.5.171 (which copies all `AttributeMatrix` arrays). The transfer collects arrays via `findAllChildrenOfType` and dispatches the tuple copy by concrete type — `IDataArray::copyTuple` / `INeighborList::copyTuple` / `StringArray::operator[]` (`GenerateTransferArrayList` and `CopyArrayTuple` in `Algorithms/NeighborOrientationCorrelation.cpp`). Verified by the `Oracle F13` unit test. -- **`neighborDiffCount` is dead code**: the count of neighbors *different* from the reference cell is accumulated but never read, in every version since 2013; the documented "at least *Cleanup Level* neighbors must be different than the reference cell" threshold has never been enforced (`currentLevel` never appears in the loop body — the Level parameter only sets the pass count). Documentation corrected during this V&V. -- **`bestNeighbor` persists across passes**: a cell replaced in pass *n* is re-copied from the same neighbor in later passes even if its confidence is now above the threshold. Identical in 6.5.171 and SIMPLNX; mirrored by the reference oracle. -- **Level ≥ 6 means zero passes**: the filter is a no-op at its default parameter value in both versions (invariant I4 in the unit tests). +**Evidence:** `Phase and Laue Index Bounds` asserts both errors and successful execution with phase-zero neighbors. All release sources through v7.4.2 and DREAM.3D tag `v6.5.171` lack these bounds checks. PR #1568 (`4ec21e93f`) adds the current guards. diff --git a/src/Plugins/OrientationAnalysis/vv/deviations/ReadAngDataFilter.md b/src/Plugins/OrientationAnalysis/vv/deviations/ReadAngDataFilter.md index 9bd012a31c..34c1beca58 100644 --- a/src/Plugins/OrientationAnalysis/vv/deviations/ReadAngDataFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/deviations/ReadAngDataFilter.md @@ -1,14 +1,10 @@ -# Deviations from DREAM3D 6.5.171: ReadAngDataFilter +# Deviations from DREAM.3D v6.5.171: ReadAngDataFilter -This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent. +Part 1 lists differences from DREAM.3D v6.5.171 (D1–D5). Part 2 records the shared reader-buffer defect fixed in SIMPLNX (D6). The entries share one stable ID sequence. -Entries are referenced by stable ID (`ReadAngDataFilter-D`) from the V&V report and from public migration guidance. The ID is stable across renames; the Filter UUID field is the permanent cross-reference anchor. +## Part 1 — Differences from DREAM.3D v6.5.171 -Comparison run 2026-07-07 against the official DREAM3D 6.5.171 release on three fixtures (hand-authored 2-phase toy `.ang`, Small IN100 `Slice_1.ang`, and a non-contiguous-phase-index variant of the toy file). **All numeric outputs — cell arrays, ensemble arrays, and geometry — were bit-identical on the two supported-format fixtures.** The deviations below are the complete list of differences. - ---- - -## ReadAngDataFilter-D1 +### ReadAngDataFilter-D1 | Field | Value | |---|---| @@ -16,17 +12,19 @@ Comparison run 2026-07-07 against the official DREAM3D 6.5.171 release on three | **Filter UUID** | `5b062816-79ac-47ce-93cb-e7966896bcbd` | | **Status** | active | -**Symptom:** Material names in the Ensemble Attribute Matrix carry a trailing space in 6.5.171 (`"Nickel "`) but are trimmed in SIMPLNX (`"Nickel"`). +**Symptom:** DREAM.3D v6.5.171 stores material names such as `"Nickel "`; SIMPLNX stores `"Nickel"`. -**Root cause:** Algorithmic choice. EbsdLib's `AngPhase::parseMaterialName()` (both legacy and 3.0.0) rejoins the header tokens with a trailing space. SIMPLNX `ReadAngData::loadMaterialInfo()` applies `StringUtilities::trimmed()` before storing the name; 6.5.171 stored the raw string. Demonstrated on both comparison fixtures (2026-07-07). +**Root cause:** Algorithmic choice. `ReadAngData::loadMaterialInfo()` trims the name before storing it. DREAM.3D v6.5.171 stores the reader's name without trimming. -**Affected users:** Any workflow that string-matches material names against `.dream3d` files produced by 6.5.171 (e.g., scripting against `MaterialName`). Visualization users will not notice. +**Affected users:** Users who compare material-name strings with DREAM.3D v6.5.171 output. -**Recommendation:** Trust SIMPLNX. The trailing space is a parsing artifact, not information; the trimmed name is the name as written in the `.ang` header. +**Recommendation:** Trust SIMPLNX. Trim surrounding whitespace when comparing material names across applications. + +**Evidence:** The current algorithm and the tagged DREAM.3D v6.5.171 `ReadAngData.cpp` show the two assignments. `vv_work:ReadAngDataFilter/ComparisonSummary.md` records the trailing-space difference on the toy and Small IN100 fixtures on 2026-07-07. The analytical oracle checks both trimmed names. --- -## ReadAngDataFilter-D2 +### ReadAngDataFilter-D2 | Field | Value | |---|---| @@ -34,36 +32,41 @@ Comparison run 2026-07-07 against the official DREAM3D 6.5.171 release on three | **Filter UUID** | `5b062816-79ac-47ce-93cb-e7966896bcbd` | | **Status** | active | -**Symptom:** For `.ang` files whose header contains `# TEM data` or `# File Created from ACOM RES results`, 6.5.171 sets the Image Geometry length unit to **Nanometer**; SIMPLNX always sets **Micrometer**. +**Symptom:** Headers containing `# TEM data` or `# File Created from ACOM RES results` receive Nanometer geometry units in DREAM.3D v6.5.171 and Micrometer units in SIMPLNX. + +**Root cause:** Algorithmic choice. DREAM.3D v6.5.171 detects these markers in `readDataFile()`. SIMPLNX always creates the Image Geometry with `LengthUnit::Micrometer`; both use the header's numeric spacing values. -**Root cause:** Algorithmic choice. Legacy `ReadAngData::readDataFile()` scanned the original header for the two TEM/ACOM marker strings and switched the unit; the SIMPLNX port hard-codes `LengthUnit::Micrometer` (`ReadAngDataFilter.cpp`, preflight geometry action). Deliberately not restored: EDAX retired those TEM/ACOM `.ang` variants more than a decade ago and no longer supports them, so no supported instrument produces such files. Source-level finding; not demonstrable with any supported fixture. +**Affected users:** Users who import files with either marker and use geometry units to interpret physical dimensions. -**Affected users:** Only users importing archival TEM/ACOM `.ang` files from retired EDAX software. The unit is display/metadata only — the numeric spacing values are identical. +**Recommendation:** Set the imported geometry units to Nanometer when those units describe the source scan. The unchanged spacing numbers do not compensate for a different unit label. -**Recommendation:** Either acceptable. Users with archival TEM/ACOM files should manually set the geometry units to Nanometer (e.g., Set Image Geometry Units filter) after import. +**Evidence:** Tagged DREAM.3D v6.5.171 `ReadAngData.cpp` contains both marker checks and unit assignments. Current `ReadAngDataFilter.cpp` supplies Micrometer to `CreateImageGeometryAction`. No current fixture contains either marker. --- -## ReadAngDataFilter-D3 +### ReadAngDataFilter-D3 | Field | Value | |---|---| | **Deviation ID** | `ReadAngDataFilter-D3` | | **Filter UUID** | `5b062816-79ac-47ce-93cb-e7966896bcbd` | | **Status** | active | -| **Bug flag** | **Legacy bug — crash, empirically confirmed** | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | + +**Symptom:** A file that declares only phase 2 can cause an out-of-bounds ensemble write and a crash. SIMPLNX now imports that file with three ensemble tuples. -**Symptom:** A `.ang` file whose phase sections do not start at index 1 (e.g., only a `# Phase 2` section) **crashes DREAM3D 6.5.171 with a segmentation fault** (PipelineRunner exit code 139, confirmed 2026-07-07). SIMPLNX imports the file correctly. +**Root cause:** Bug. The affected versions allocate `phaseCount + 1` tuples but write each phase at its declared index. SIMPLNX now allocates `max(maxPhaseIndex, phaseCount) + 1` tuples and initializes every slot to invalid-phase defaults. It returns `-19502` for an index below 1 and `-19504` for an index at or above the allocated count. -**Root cause:** Bug in 6.5.171. Legacy `ReadAngData::loadMaterialInfo()` sizes the ensemble arrays to `phases.size() + 1` but writes each phase at `phase->getPhaseIndex()`; when indices are non-contiguous the write is out of bounds (a 2-tuple array written at index 2). SIMPLNX (resolved) sizes the ensemble arrays to `maxPhaseIndex + 1` in preflight, initializes every slot to the "Invalid Phase" defaults (`CrystalStructures = 999`, `MaterialName = "Invalid Phase"`, zero lattice constants), overwrites the slots that have phase sections, and returns error `-19502` if a phase index falls outside the arrays. +**Affected users:** Users with sparse phase indices. A file change after preflight can also put a newly declared phase outside the allocated arrays. -**Affected users:** Anyone importing a `.ang` file with non-contiguous phase indices into 6.5.171 (crash, potential silent memory corruption in earlier writes). Standard EDAX exports number phases contiguously from 1, so typical files are unaffected. +**Recommendation:** Trust SIMPLNX in DREAM3D-NX v7.4.2 or later. Keep the input file unchanged during import. -**Recommendation:** Trust SIMPLNX. The 6.5.171 behavior is undefined (out-of-bounds write). Pinned by the `Non-Contiguous Phase Index` unit test in `test/ReadAngDataTest.cpp`. +**Evidence:** Every release source from `upstream/release/v7.0.0` through `upstream/release/v7.4.1` has count-based allocation and unchecked writes. The v7.4.2 source contains the PR #1657 correction (`49ea8b09c`). `Non-Contiguous Phase Index` asserts three tuples and all unused-slot defaults. `vv_work:ReadAngDataFilter/ComparisonSummary.md` records the stock DREAM.3D v6.5.171 crash on 2026-07-07; `vv_work:ReadAngDataFilter/ReadMe.md` and `vv_work:ReadAngDataFilter/LegacyPatch.md` record the successful local proof version on 2026-09-17. --- -## ReadAngDataFilter-D4 +### ReadAngDataFilter-D4 | Field | Value | |---|---| @@ -71,17 +74,19 @@ Comparison run 2026-07-07 against the official DREAM3D 6.5.171 release on three | **Filter UUID** | `5b062816-79ac-47ce-93cb-e7966896bcbd` | | **Status** | active | -**Symptom:** Error codes and messages for rejected files differ: HexGrid files fail with `-1000` in 6.5.171 vs `-19500` in SIMPLNX; a missing `GRID` header key fails with `-300` (from EbsdLib at execute) in 6.5.171 vs `-19501` at preflight in SIMPLNX; missing-file and wrong-extension errors (`-388` / `-997` in legacy `dataCheck()`) are handled by the `FileSystemPathParameter` in SIMPLNX with framework codes. +**Symptom:** HexGrid gives `-1000` in DREAM.3D v6.5.171 and `-19500` in SIMPLNX. A missing GRID key gives reader error `-300` during legacy execution and `-19501` during SIMPLNX preflight. -**Root cause:** Algorithmic choice (framework error-handling differences). No data output is affected — these are rejection paths. +**Root cause:** Algorithmic choice. SIMPLNX adds a missing-GRID check to preflight and uses its own filter error codes. `FileSystemPathParameter` performs file-path validation. -**Affected users:** Only scripts that match on specific error codes/messages from pipeline logs. +**Affected users:** Scripts that match error codes or depend on the stage at which an invalid input fails. -**Recommendation:** Either acceptable. Update any error-code matching to the SIMPLNX codes (`-19500` HexGrid, `-19501` missing GRID, `-19502` phase index < 1, `-19503` reader/geometry element-count mismatch, `-19504` phase index above the ensemble count). +**Recommendation:** Either rejection is acceptable. Update error-code matching to the SIMPLNX codes. + +**Evidence:** The tagged legacy filter and `Source/EbsdLib/TSL/AngReader.cpp` contain `-1000` and `-300`. Current preflight and the `HexGrid Preflight Error (-19500)` and `Missing GRID Preflight Error (-19501)` cases establish the SIMPLNX behavior. --- -## ReadAngDataFilter-D5 +### ReadAngDataFilter-D5 | Field | Value | |---|---| @@ -89,10 +94,36 @@ Comparison run 2026-07-07 against the official DREAM3D 6.5.171 release on three | **Filter UUID** | `5b062816-79ac-47ce-93cb-e7966896bcbd` | | **Status** | active | -**Symptom:** A `.ang` file containing a `# Phase 0` section is accepted by DREAM3D 6.5.171 but rejected by SIMPLNX at execute with error `-19502`. +**Symptom:** DREAM.3D v6.5.171 writes a `# Phase 0` section into ensemble slot 0. SIMPLNX rejects that section with `-19502`. + +**Root cause:** Algorithmic choice. DREAM.3D v6.5.171 skips negative header indices but accepts zero. SIMPLNX reserves slot 0 for invalid-phase defaults and rejects every header index below 1. Cell phase values below 1 still map to 1 during copying. + +**Affected users:** Users whose ANG headers declare phase 0 or a negative phase index. + +**Recommendation:** Trust SIMPLNX. Use positive phase indices in the header and update the corresponding indexed cell values. + +**Evidence:** Tagged DREAM.3D v6.5.171 `loadMaterialInfo()` checks `phaseID < 0`; current SIMPLNX checks `phaseID < 1`. `Phase 0 rejected (-19502)` checks this static input. The analytical oracle separately checks the cell-phase remap. + +--- + +## Part 2 — SIMPLNX defects and behavior changes + +### ReadAngDataFilter-D6 + +| Field | Value | +|---|---| +| **Deviation ID** | `ReadAngDataFilter-D6` | +| **Filter UUID** | `5b062816-79ac-47ce-93cb-e7966896bcbd` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected by inconsistent column counts. | +| **Fixed in** | DREAM3D-NX v7.4.2 | + +**Symptom:** Import can read outside the reader buffers when the geometry contains more cells than the reader supplies. + +**Root cause:** Bug. The copy loops use the geometry cell count without checking the reader count. For a square grid, `getXDimension()` uses `NCOLS_EVEN`, while reader allocation uses a positive `NCOLS_ODD` first. A malformed header with `NCOLS_EVEN > NCOLS_ODD` can therefore make the copies exceed the buffers. In SIMPLNX, a smaller file read after preflight can cause the same mismatch. The filter now returns `-19503` before accessing those buffers. -**Root cause:** Algorithmic choice. Legacy `ReadAngData` skipped only *negative* phase indices; a Phase 0 section was written into ensemble slot 0 (the slot SIMPLNX reserves for the "Invalid Phase" defaults) and the import succeeded. TSL `.ang` phase numbering starts at 1, so SIMPLNX treats a phase index `< 1` as malformed and rejects it. (Legacy also had no dedicated code path for it — it happened to not crash.) This is enforced in `loadMaterialInfo` and pinned by the static `Phase 0 rejected (-19502)` unit test, which trips the guard deterministically without any file-mutation injection. +**Affected users:** Users with inconsistent square-grid column counts, or an input file that shrinks between SIMPLNX preflight and execution. -**Affected users:** Only users with nonstandard `.ang` files that declare a Phase 0 section — rare, and such files carry no meaningful phase-0 crystallography in legacy output anyway (slot 0 collided with the invalid-phase slot). +**Recommendation:** Trust SIMPLNX in DREAM3D-NX v7.4.2 or later. Correct inconsistent column counts and keep the input file unchanged during import. -**Recommendation:** Trust SIMPLNX. Renumber the file's phases to start at 1. A Phase 0 section is outside the TSL `.ang` specification. +**Evidence:** All release sources through `upstream/release/v7.4.1` lack the reader-count guard; v7.4.2 contains the PR #1657 fix (`49ea8b09c`). Tagged DREAM.3D v6.5.171 `ReadAngData::copyRawEbsdData()` also copies the geometry count without a bounds check; tagged `Source/EbsdLib/TSL/AngReader.cpp` confirms the even-column geometry and odd-column allocation. No current test directly triggers `-19503`. diff --git a/src/Plugins/OrientationAnalysis/vv/deviations/ReadCtfDataFilter.md b/src/Plugins/OrientationAnalysis/vv/deviations/ReadCtfDataFilter.md index 0b1cd25cc6..86441374b7 100644 --- a/src/Plugins/OrientationAnalysis/vv/deviations/ReadCtfDataFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/deviations/ReadCtfDataFilter.md @@ -1,14 +1,12 @@ -# Deviations from DREAM3D 6.5.171: ReadCtfDataFilter +# Deviations from DREAM.3D v6.5.171: ReadCtfDataFilter -This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent. - -Entries are referenced by stable ID (`ReadCtfDataFilter-D`) from the V&V report and from public migration guidance. The ID is stable across renames; the Filter UUID field is the permanent cross-reference anchor. - -Comparison run 2026-07-24 against the official DREAM3D 6.5.171 release (`PipelineRunner`): four runs over three byte-identical input files — a hand-authored 3×2 toy `.ctf` (two conversion-option combinations), the 550×400 production scan `Cugrid_after 2nd_15kv_2kx_2.ctf` (two phases — Cu3Ga hexagonal + Copper cubic — with 116,050 unindexed points), and a 2×2×2 multi-slice toy `.ctf`. On every run, all numeric outputs are **bit-identical** except the unindexed-point (Phase 0) family documented as D1/D2 (on the production scan: 543,950 of 660,000 Euler values match; the 116,050 differing values are all unindexed points' φ2). Three additional malformed-input fixtures were run through 6.5.171 to document its failure behavior (D3). +Part 1 lists differences from DREAM.3D v6.5.171. Part 2 lists SIMPLNX defects whose fixes restore the legacy behavior. All entries use one stable ID sequence. --- -## ReadCtfDataFilter-D1 +## Part 1 — Differences from DREAM.3D v6.5.171 + +### ReadCtfDataFilter-D1 | Field | Value | |---|---| @@ -16,17 +14,19 @@ Comparison run 2026-07-24 against the official DREAM3D 6.5.171 release (`Pipelin | **Filter UUID** | `7751923c-afb9-4032-8372-8078325c69a4` | | **Status** | active | -**Symptom:** Unindexed scan points ("zero solutions") carry `Phases == 0` in SIMPLNX but `Phases == 1` in 6.5.171. On the Cugrid production fixture this affects 116,050 of 220,000 points. +**Symptom:** Unindexed points have phase 0 in SIMPLNX and phase 1 in DREAM.3D v6.5.171. -**Root cause:** Algorithmic choice. 6.5.171's `ReadCtfData::copyRawEbsdData()` remaps every phase value `< 1` to `1` before storing the Phases array (a convention inherited from `H5CtfVolumeReader`: "the lowest value is One (1)"). SIMPLNX deliberately removed this remap in PR #937 (May 2024): a phase value of 0 is meaningful in `.ctf` files — it marks a point the indexing software could not solve — and rewriting it to 1 silently assigns those points to a real phase. +**Root cause:** Algorithmic choice. DREAM.3D changes every phase value below 1 to 1 before storing the phase array. SIMPLNX preserves phase zero because it identifies an unindexed point. -**Affected users:** Anyone importing `.ctf` files that contain unindexed points — which is nearly every real-world Oxford/HKL scan. Downstream workflows that segmented or masked using `Phases == 0` under legacy DREAM3D never saw phase 0 and instead thresholded on `Error > 0`; that workflow continues to work identically in SIMPLNX (`Error` is copied verbatim, bit-identical). +**Affected users:** Users who import CTF files with unindexed points and select or mask data by phase. -**Recommendation:** Trust SIMPLNX. Preserving the measured phase value is strictly more information; the legacy remap destroyed the unindexed marker and mislabeled unindexed points as phase 1. Users who need legacy-parity masks can threshold on `Error = 0` (recommended in the filter documentation) or replace phase-0 values explicitly. +**Recommendation:** Trust SIMPLNX. Preserve phase zero to distinguish unindexed points from measured phase 1; use an explicit replacement if a workflow requires the old convention. + +**Evidence:** The DREAM.3D `v6.5.171` source at `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/ReadCtfData.cpp` contains the remap. PR #937 removes it from SIMPLNX. `Class 1 Analytical Oracle` asserts phases `{1, 2, 0, 1, 2, 0}`. The stock comparison on 2026-07-24 records 116,050 changed phase values in the 220,000-point Cugrid scan: `vv_work:ReadCtfDataFilter/results/compare_ab_2026-07-24.txt`. --- -## ReadCtfDataFilter-D2 +### ReadCtfDataFilter-D2 | Field | Value | |---|---| @@ -34,17 +34,19 @@ Comparison run 2026-07-24 against the official DREAM3D 6.5.171 release (`Pipelin | **Filter UUID** | `7751923c-afb9-4032-8372-8078325c69a4` | | **Status** | active | -**Symptom:** When "Convert Hexagonal X-Axis to EDAX Standard" is enabled and the file's phase 1 is hexagonal (Laue group 9), the φ2 Euler angle of **unindexed** points differs by exactly 30° (0.5235988 rad): 6.5.171 applies the +30° shift to them, SIMPLNX does not. On the Cugrid fixture, all 116,050 unindexed points show legacy φ2 = 0.5235988 vs SIMPLNX φ2 = 0.0. Euler angles of every indexed point are bit-identical. +**Symptom:** With EDAX alignment enabled and a 6/mmm phase 1, DREAM.3D v6.5.171 adds 30 degrees to unindexed points' phi2; SIMPLNX leaves those values unchanged. + +**Root cause:** Algorithmic choice. D1 makes DREAM.3D use phase 1's crystal structure for unindexed points. SIMPLNX uses the unknown-structure slot at phase zero, which does not receive a hexagonal correction. -**Root cause:** Algorithmic choice (consequence of D1). Both versions decide the +30° hexagonal-alignment shift by looking up `CrystalStructures[Phases[i]]`. Legacy has already remapped unindexed points to phase 1, so they inherit phase 1's hexagonal classification and receive the shift; SIMPLNX looks up ensemble slot 0 (`UnknownCrystalStructure`, 999), which is never hexagonal, so unindexed points keep their file values. +**Affected users:** Users who retain unindexed Euler angles with EDAX alignment enabled and a 6/mmm phase 1. -**Affected users:** Only workflows that read Euler angles of unindexed points under the EDAX-alignment option — those angles are meaningless by definition (the point was never indexed; the file stores 0.0 there). +**Recommendation:** Trust SIMPLNX. A point without an indexed phase must not inherit the orientation correction for a measured phase. -**Recommendation:** Trust SIMPLNX. Applying a crystallographic correction to points with no crystallographic solution was an artifact of the D1 remap, not a deliberate feature. +**Evidence:** `Euler Conversion Combinations` asserts unchanged unindexed values in all four modes. The 2026-07-24 stock comparison records 116,050 differing phi2 values in the Cugrid scan, each approximately 0.5235988 radians apart; the other 543,950 Euler components match exactly. See `vv_work:ReadCtfDataFilter/results/compare_ab_2026-07-24.txt`. --- -## ReadCtfDataFilter-D3 +### ReadCtfDataFilter-D3 | Field | Value | |---|---| @@ -52,57 +54,100 @@ Comparison run 2026-07-24 against the official DREAM3D 6.5.171 release (`Pipelin | **Filter UUID** | `7751923c-afb9-4032-8372-8078325c69a4` | | **Status** | active | -**Symptom:** Malformed `.ctf` files that crash or silently corrupt 6.5.171 are rejected by SIMPLNX with descriptive errors. Empirically demonstrated on three fixtures against the official 6.5.171 release: +**Symptom:** Malformed CTF input can crash DREAM.3D v6.5.171 or produce incorrect output; SIMPLNX rejects the input with an error. + +**Root cause:** Bug. The import paths lacked checks for missing column buffers, empty phase definitions, phase bounds, and inconsistent dimensions. SIMPLNX now checks these conditions before copying or indexing data. It also rejects phase counts that exceed the preflight allocation after a file change. Errors `-19600` through `-19605` identify these conditions; the current reader rejects the zero-Z fixture earlier with `-101105`. + +**Affected users:** Users of DREAM3D-NX v7.0.0 through v7.4.1 who import malformed files or change a file during import. DREAM.3D v6.5.171 was affected by the missing-column, empty-phase, and out-of-range-phase defects. + +**Recommendation:** Trust SIMPLNX. DREAM3D-NX v7.4.2 contains the guards; correct the reported input error before importing again. + +**Evidence:** All release sources from `upstream/release/v7.0.0` through `upstream/release/v7.4.1` lack the guards; v7.4.2 contains them. The tagged legacy source performs unchecked copies and phase lookups. `vv_work:ReadCtfDataFilter/ReadMe.md` records two stock crashes and an out-of-range phase accepted on 2026-07-24. `vv_work:ReadCtfDataFilter/LegacyPatch.md` records a local proof version that rejects all three inputs on 2026-09-17 and preserves 60 datasets across four well-formed cases. Current error tests assert `-19600`, `-19601`, `-19602`, and `-19604`; `-19603` and `-19605` require file-change injection. + +--- + +### ReadCtfDataFilter-D5 + +| Field | Value | +|---|---| +| **Deviation ID** | `ReadCtfDataFilter-D5` | +| **Filter UUID** | `7751923c-afb9-4032-8372-8078325c69a4` | +| **Status** | active | + +**Symptom:** EDAX alignment omitted the 30-degree phi2 correction for 6/m phases. -| Malformed input | DREAM3D 6.5.171 (demonstrated) | SIMPLNX | -|---|---|---| -| Data section missing a standard column (no `BS`) | **Segfault** (exit 139; `memcpy` from null buffer) | Error `-19601` naming the missing column | -| `Phases 0` header (no phase definitions) | **Segfault** (exit 139; reads a zero-tuple ensemble array out of bounds) | Error `-19600` | -| Data row phase value out of range (`Phase 7` with 2 phases) | **Silent success** (exit 0) — the +30° decision reads `CrystalStructures[7]` out of bounds, so the output depends on heap contents | Error `-19602` naming the point and valid range | -| Header `XCells`/`YCells` missing or 0 | Error at execute after a zero-sized preflight | Rejected at preflight with `-19604` | -| Header `ZCells 0` | Not run (legacy sizes the geometry from the same header) | Rejected at execute with `-19603` | +**Root cause:** Bug. The condition selected only `Hexagonal_High` (6/mmm). The Oxford/HKL-to-EDAX axis correction applies to the hexagonal lattice in both 6/m and 6/mmm. The current condition accepts both classes and applies the correction before converting to radians. -**Root cause:** Bug in 6.5.171 (out-of-bounds access on malformed input) — fixed in SIMPLNX by value-add guards added during this V&V pass. On well-formed files the guards never fire and outputs are unaffected. +**Affected users:** Users of DREAM3D-NX v7.0.0 through v7.4.2 who import 6/m data with EDAX alignment enabled. DREAM.3D v6.5.171 was affected. -**Affected users:** Anyone importing truncated, hand-edited, or non-standard `.ctf` files. No effect on well-formed files. +**Recommendation:** Trust SIMPLNX. The fix will be in the DREAM3D-NX release after v7.4.2; re-import 6/m data with that correction when EDAX alignment is required. -**Recommendation:** Trust SIMPLNX. A clean error is strictly better than a crash or a heap-dependent output. +**Evidence:** All ten release sources and the tagged legacy source select only `Hexagonal_High`. PR #1781 (`84c66066a`) introduces `IsHexagonalCrystalStructure()` in `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/utilities/EbsdReaderUtilities.hpp`. `Euler Conversion Combinations` checks both hexagonal classes, cubic points, and phase-zero points in all four modes. --- -## ReadCtfDataFilter-D4 +## Part 2 — SIMPLNX defects and behavior changes + +### ReadCtfDataFilter-D4 | Field | Value | |---|---| | **Deviation ID** | `ReadCtfDataFilter-D4` | | **Filter UUID** | `7751923c-afb9-4032-8372-8078325c69a4` | -| **Status** | retired 2026-07-24 (resolved during this V&V pass — recorded for the migration guide) — fixed in DREAM3D-NX **7.4.2** | +| **Status** | retired 2026-07-24 | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | + +**Symptom:** A multi-slice CTF file produced a single-slice geometry without a warning. -**Symptom:** Before this V&V pass, SIMPLNX imported only slice 0 of a multi-slice (3D) `.ctf` file (`ZCells > 1`) — silently — while 6.5.171 imported the full 3D volume. +**Root cause:** Bug. Preflight used a fixed Z extent and spacing of 1. It ignored the header's ZCells and ZStep, so the copy loop read only the first slice. The current filter uses ZCells and positive ZStep, with spacing 1 when ZStep is not positive. -**Root cause:** Bug in the SIMPLNX port (the preflight hard-coded a z-extent of 1 and a z-spacing of 1.0, dropping the header's `ZCells`/`ZStep`). Restored during this pass; on the 2×2×2 fixture the SIMPLNX output now matches 6.5.171 bit-for-bit (dims, ZStep-derived spacing, and all nine cell arrays). +**Affected users:** Users who imported multi-slice CTF files in DREAM3D-NX v7.0.0 through v7.4.1. -**Affected users:** Anyone who imported a multi-slice `.ctf` with an earlier DREAM3D-NX release received a single-slice geometry with slice-0 data and no warning. +**Recommendation:** Trust SIMPLNX. Re-import these files with DREAM3D-NX v7.4.2 or later to recover all slices. -**Recommendation:** Trust SIMPLNX (current). Re-import multi-slice `.ctf` files with a build containing this fix. +**Evidence:** Every release source through `upstream/release/v7.4.1` fixes Z to 1; v7.4.2 and the tagged legacy source read the header's Z extent. `vv_work:ReadCtfDataFilter/results/release_matrix.md` records the 2026-09-17 comparison: v7.4.1 gives 2×2×1, while v7.4.2 gives 2×2×2 with Z spacing 0.75. All 24 Euler values match DREAM.3D v6.5.171; two phase values differ as D1 specifies. `3D Multi-Slice CTF` checks both slice-spacing variants. --- -## ReadCtfDataFilter-D5 +### ReadCtfDataFilter-D6 | Field | Value | |---|---| -| **Deviation ID** | `ReadCtfDataFilter-D5` | +| **Deviation ID** | `ReadCtfDataFilter-D6` | +| **Filter UUID** | `7751923c-afb9-4032-8372-8078325c69a4` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | + +**Symptom:** Some converted Euler values differed from the double-intermediate result after rounding to float32. + +**Root cause:** Bug. The port used a float32 radians constant and float32 arithmetic. The current filter uses double-precision intermediates for the hexagonal shift and radian conversion, then stores float32 values. DREAM.3D v6.5.171 also used double-precision constants. + +**Affected users:** Users who compare converted Euler values exactly or rely on the final float32 rounding. + +**Recommendation:** Trust SIMPLNX. DREAM3D-NX v7.4.2 restores the more accurate conversion; re-import data when exact converted values matter. + +**Evidence:** All release sources through v7.4.1 use `k_PiOver180F`; v7.4.2 uses `k_PiOver180D` and explicit float64 intermediates. The tagged legacy filter uses `SIMPLib::Constants::k_PiOver180`, declared as double in `Source/SIMPLib/Math/SIMPLibMath.h` at the SIMPL snapshot `0afaa6854` selected by the legacy tag date. `Euler Conversion Combinations` checks 7.125, 3.375, 6.75, and shifted 37.125 degrees with literals that distinguish the two conversion methods. + +--- + +### ReadCtfDataFilter-D7 + +| Field | Value | +|---|---| +| **Deviation ID** | `ReadCtfDataFilter-D7` | | **Filter UUID** | `7751923c-afb9-4032-8372-8078325c69a4` | -| **Affected releases** | none through 7.4.2 — those releases matched 6.5.171 (6/mmm only) | -| **Status** | active — intentional correction introduced by PR #1781 (2026-10-14) | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | -**Symptom:** With EDAX hexagonal alignment enabled, simplnx adds 30 degrees to phi2 for both 6/m and 6/mmm phases; legacy DREAM3D 6.5 shifted only 6/mmm phases. +**Symptom:** A reader failure could produce a filter error with code zero instead of its negative error code. -**Root cause:** The Oxford/HKL versus EDAX/TSL x-axis convention depends on the hexagonal lattice, so the correction also applies to `Hexagonal_Low` (6/m). +**Root cause:** Bug. The filter read only the reader's error-code member, which some failures leave at zero. The current preflight and execution paths use the negative return code when that member is not negative. DREAM.3D v6.5.171 used the return code directly. -**Affected users:** Users importing 6/m phases with EDAX hexagonal alignment enabled. +**Affected users:** Users who import files with a zero scan step or other reader failures that set only a return code and message. -**Correct behavior:** Both hexagonal Laue classes receive the same shift, before optional conversion to radians. Cubic and unindexed points remain unchanged; trigonal phases are outside this correction. Covered by `test/ReadCtfDataTest.cpp::"Euler Conversion Combinations"`. +**Recommendation:** Trust SIMPLNX. DREAM3D-NX v7.4.2 reports the failure code needed to diagnose these inputs. -**Recommendation:** Re-import 6/m data when EDAX alignment is required. +**Evidence:** All release sources through v7.4.1 call `MakeErrorResult(reader.getErrorCode(), ...)`; v7.4.2 adds the fallback in both read paths. The tagged legacy `readDataFile()` uses `setErrorCondition(err)`. `EbsdLib Error Passthrough - Zero Step (-102)` checks the execution fallback. The header-read fallback has no direct test. diff --git a/src/Plugins/OrientationAnalysis/vv/deviations/ReadEbsdPatternFileFilter.md b/src/Plugins/OrientationAnalysis/vv/deviations/ReadEbsdPatternFileFilter.md index b3751c0681..a61e32c119 100644 --- a/src/Plugins/OrientationAnalysis/vv/deviations/ReadEbsdPatternFileFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/deviations/ReadEbsdPatternFileFilter.md @@ -1,6 +1,5 @@ -# Deviations from DREAM3D 6.5.171: ReadEbsdPatternFileFilter +# Deviations from DREAM.3D v6.5.171: ReadEbsdPatternFileFilter -`ReadEbsdPatternFileFilter` is a new filter with UUID -`cd975a45-53de-4ace-ac16-d99638a44d6a`. +`ReadEbsdPatternFileFilter` is a new filter with UUID `cd975a45-53de-4ace-ac16-d99638a44d6a`. -DREAM3D 6.5.171 has no equivalent filter. Therefore, there are no legacy deviations to report. +DREAM.3D v6.5.171 has no equivalent filter. There are no Part 1 entries. The V&V found no confirmed defects or behavior changes that require Part 2 entries. diff --git a/src/Plugins/OrientationAnalysis/vv/deviations/ReadH5OinaDataFilter.md b/src/Plugins/OrientationAnalysis/vv/deviations/ReadH5OinaDataFilter.md index d623361b1c..d70f49a374 100644 --- a/src/Plugins/OrientationAnalysis/vv/deviations/ReadH5OinaDataFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/deviations/ReadH5OinaDataFilter.md @@ -1,324 +1,401 @@ -# Deviations: ReadH5OinaDataFilter +# Deviations from DREAM.3D v6.5.171: ReadH5OinaDataFilter -**There is no DREAM3D 6.5.171 equivalent of this filter**, so there is no legacy comparison and no legacy-versus-SIMPLNX deviation to record. The filter was written in SIMPLNX (PR #700, `a51dd5f3d`, 2024-03-25) and has no `FromSIMPLJson`, no legacy-UUID mapping entry and no SIMPL conversion fixtures. +DREAM.3D v6.5.171 has no H5OINA importer. Part 2 records SIMPLNX defects and behavior changes; there are no Part 1 entries. IDs D1 through D14 keep their existing meanings. D15 through D18 identify the additional input-validation defects. -This file therefore records, in the same structured form, every difference between the behavior DREAM3D-NX shipped and the correct behavior. Entries are referenced by stable ID (`ReadH5OinaDataFilter-D`) from the V&V report and from public migration guidance. The ID is stable across renames; the Filter UUID field is the permanent cross-reference anchor. - -The filter first shipped in DREAM3D-NX 7.0.0. D1 through D13 affect every released version from 7.0.0 through 7.4.1. D5, D6, D7, D10, D11, and D12 are corrections to EbsdLib's `H5OINAReader` and require EbsdLib 3.1.2. D14 affects every released version from 7.0.0 through 7.4.2 and is resolved on `develop` by PR #1781. +## Part 2 — SIMPLNX defects and behavior changes --- -## ReadH5OinaDataFilter-D1 +### ReadH5OinaDataFilter-D1 | Field | Value | |---|---| | **Deviation ID** | `ReadH5OinaDataFilter-D1` | | **Filter UUID** | `fad3d47f-f1e1-4429-bc65-5e021be62ba0` | -| **Affected releases** | 7.0.0 through 7.4.1 | -| **Status** | resolved — fixed in DREAM3D-NX **7.4.2** | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | -**Symptom:** With "Convert Hexagonal X-Axis to EDAX Standard" on — its shipped default — every scan point whose phase maps to `Hexagonal_High` had 30.0 added to its φ2. The file's Euler angles are radians, so this added **thirty radians**, not thirty degrees. Thirty radians is 4.867 radians modulo 2π, so the resulting orientation bears no relation to either convention: the correction is 57.3 times the intended one (the ratio is exactly 180/π) and lands at an arbitrary angle. Every downstream product of those orientations — pole figures, IPF colors, misorientations, grain segmentation — was wrong for every hexagonal point of every H5OINA file imported at default settings. Cubic and unindexed points were unaffected. +**Symptom:** Hexagonal alignment added 30 radians instead of 30 degrees to phi2. -**Root cause:** Bug. The correction is a 30 degree rotation about [0001] applied to φ2, and the `.ctf` importer, whose files store degrees, correctly adds the literal `30.0`. The H5OINA path reused that literal even though H5OINA stores radians and the filter performs no degrees-to-radians conversion anywhere. +**Root cause:** bug. The importer reused the degree-valued literal 30.0 although H5OINA Euler angles are in radians. Adds pi/6 radians with a double-precision intermediate. -**Affected users:** Anyone importing an `.h5oina` file containing a hexagonal phase — titanium, magnesium, zirconium, zinc and hcp alloys generally — without turning the option off. Because the option defaults to on, this was the default outcome. Files with no hexagonal phase, including the archived 25 × 25 titanium-cubic test file, were never affected, which is why the pre-existing test suite could not see it. +**Affected users:** Imports with Hexagonal-High phases and EDAX alignment enabled. -**Correct behavior:** The value added is 30 degrees expressed in radians, `30 × (π/180)`, computed on a double-precision intermediate so the stored float32 is the correctly-rounded result — the same arithmetic shape the `.ctf` importer uses. Pinned by `test/ReadH5OinaDataTest.cpp::"Class 1 Analytical Oracle"`, `…::"Conversion Option Combinations"` and `…::"Multi-Scan Hexagonal Alignment"`, whose φ2 expectations distinguish the correct value both from the literal-30 result and from a float32-intermediate result. +**Recommendation:** Trust SIMPLNX. Re-import affected data and repeat downstream orientation analysis. -**Recommendation:** Trust the corrected behavior. Results produced by 7.0.0 through 7.4.1 from `.h5oina` files with a hexagonal phase and the option left on must be regenerated; there is no post-hoc correction, because the shift was applied before any downstream analysis. Users who cannot upgrade should turn the option off and apply the 30 degree rotation with [Rotate Euler Reference Frame](../../docs/RotateEulerRefFrameFilter.md). +**Evidence:** `Class 1 Analytical Oracle`, `Conversion Option Combinations`, and `Multi-Scan Hexagonal Alignment` assert independently rounded phi2 values. Release-source and dependency evidence is in `../provenance/ReadH5OinaDataFilter.md`. --- -## ReadH5OinaDataFilter-D2 +### ReadH5OinaDataFilter-D2 | Field | Value | |---|---| | **Deviation ID** | `ReadH5OinaDataFilter-D2` | | **Filter UUID** | `fad3d47f-f1e1-4429-bc65-5e021be62ba0` | -| **Affected releases** | 7.0.0 through 7.4.1 | -| **Status** | resolved — fixed in DREAM3D-NX **7.4.2** | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | -**Symptom:** When two or more scans were selected and stacked into one Image Geometry, the `Euler` array of every scan after the first landed at the wrong place. Scan *k* occupies the tuple slab starting at *k*·X·Y, so its Euler block belongs at element 3·*k*·X·Y; it was written at element *k*·X·Y instead — one third of the correct offset, because the offset was counted in tuples rather than in elements. Scan 2's Euler block therefore overwrote the last two thirds of scan 1's Euler data, and the last two thirds of scan 2's own slab were left at their zero-initialized values. Every other cell array was placed correctly, so the corruption was confined to orientations and was silent: no error, no warning, and an output whose array sizes and geometry were all correct. Single-scan imports were unaffected, because the offset is zero. +**Symptom:** Later scans overwrote earlier Euler values. -**Root cause:** Bug. The `Euler` copy correctly passes an element count of `totalPoints × 3` but passed the destination offset in tuples. The sibling `ReadH5OimData` writes the same interleave correctly as `(sliceTupleStart + i) * 3`. +**Root cause:** bug. The Euler copy used an element count of three times the point count but passed a destination offset in tuples. Multiplies the destination tuple offset by three Euler components. -**Affected users:** Anyone importing more than one scan from an `.h5oina` file in a single filter invocation. Single-scan imports — the common case, and the only case the pre-existing test covered — were never affected. +**Affected users:** Imports with two or more selected scans. -**Correct behavior:** The destination offset is three times the tuple offset. Pinned by `test/ReadH5OinaDataTest.cpp::"Multi-Scan Slab Placement"`, which uses a cubic-only two-scan fixture so no alignment transform can mask a placement error, and asserts all 24 Euler values across both slabs. +**Recommendation:** Trust SIMPLNX. Re-import multi-scan data produced by the affected releases. -**Recommendation:** Trust the corrected behavior. Multi-scan H5OINA imports produced by 7.0.0 through 7.4.1 must be regenerated. +**Evidence:** `Multi-Scan Slab Placement` checks all 24 Euler values of two cubic scans. Release-source and dependency evidence is in `../provenance/ReadH5OinaDataFilter.md`. --- -## ReadH5OinaDataFilter-D3 +### ReadH5OinaDataFilter-D3 | Field | Value | |---|---| | **Deviation ID** | `ReadH5OinaDataFilter-D3` | | **Filter UUID** | `fad3d47f-f1e1-4429-bc65-5e021be62ba0` | -| **Affected releases** | 7.0.0 through 7.4.1 | -| **Status** | resolved — fixed in DREAM3D-NX **7.4.2** | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | -**Symptom:** In a multi-scan import with the hexagonal alignment on, the alignment loop always walked scan-point indices 0 through X·Y — the first scan's tuples — regardless of which scan was being copied. In an *S*-scan stack, the first scan's hexagonal points therefore received the shift *S* times over, and no point of any later scan received it at all. Single-scan imports were unaffected. +**Symptom:** Alignment repeatedly modified the first scan and skipped later scans. -**Root cause:** Bug. `copyRawEbsdData` computed the slab offset for the copies but the alignment helper it called neither took nor applied that offset. +**Root cause:** bug. The alignment helper did not receive the scan offset and always started at tuple zero. Applies alignment once to each point in the current scan slab. -**Affected users:** Anyone importing more than one scan from an `.h5oina` file containing a hexagonal phase. Compounds with D1 and D2 on the same imports. +**Affected users:** Multi-scan imports with Hexagonal-High phases and EDAX alignment enabled. -**Correct behavior:** The alignment loop iterates the scan's own slab, so each point is visited exactly once. Pinned by `test/ReadH5OinaDataTest.cpp::"Multi-Scan Hexagonal Alignment"`, in which every point of both scans is hexagonal, so a shift applied to the wrong slab or applied twice changes a pinned value. +**Recommendation:** Trust SIMPLNX. Re-import affected multi-scan data. -**Recommendation:** Trust the corrected behavior. +**Evidence:** `Multi-Scan Hexagonal Alignment` checks distinct Euler values in both scan slabs. Release-source and dependency evidence is in `../provenance/ReadH5OinaDataFilter.md`. --- -## ReadH5OinaDataFilter-D4 +### ReadH5OinaDataFilter-D4 | Field | Value | |---|---| | **Deviation ID** | `ReadH5OinaDataFilter-D4` | | **Filter UUID** | `fad3d47f-f1e1-4429-bc65-5e021be62ba0` | -| **Affected releases** | 7.0.0 through 7.4.1 | -| **Status** | resolved — as a limitation, honestly reported — fixed in DREAM3D-NX **7.4.2** | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | -**Symptom:** Turning on "Import Pattern Data" always failed, on every file, with "The parameter 'Read Pattern Data' has been enabled but there does not seem to be any pattern data in the file for the scan name selected" — including for files that plainly do contain pattern data. The archived production AZtec file, for instance, carries a 625 × 512 × 622 `Processed Patterns` dataset and still produced that message. +**Symptom:** Pattern import reported missing data when the reader did not support pattern import. -**Root cause:** Bug in the reported reason, over a missing feature. `H5OINAReader::getPatternData()` returns `nullptr` unconditionally, `getPatternDims(std::array)` takes its argument **by value** with an empty body, and the reader's pattern-reading block is commented out. Preflight's pattern dimensions therefore stayed `{0, 0}` and the failure fired regardless of file content, attributing a missing library feature to the user's file. Behind that failure the execute-side plumbing was itself inconsistent: preflight created `Unprocessed Patterns` as `uint16` while execute fetched it as `UInt8Array`, which would have thrown had it ever been reached, and its copy loop used the tuple offset where it needed the element offset. The filter also only ever targeted `Unprocessed Patterns`, while real AZtec exports may carry only `Processed Patterns`. +**Root cause:** bug. The reader returned no pattern buffer or dimensions. Preflight interpreted the zero dimensions as absent file data. The unreachable copy also used a different element type from the output action. Rejects pattern import explicitly with error `-9583` and identifies an alternative. -**Affected users:** Anyone who turned the parameter on. No user ever obtained pattern data from an `.h5oina` file through this filter. +**Affected users:** Users who enable Import Pattern Data. -**Correct behavior:** The parameter is retained for pipeline compatibility, and preflight now reports the true reason — pattern import is not yet supported for H5OINA files — naming the file and pointing at the SimplnxCore **Read HDF5 Dataset** filter as the way to read the patterns a file does contain. The unreachable, inconsistent execute block and its error code `-34970` are removed. The parameter's help text and the filter documentation state the limitation. Pinned by `test/ReadH5OinaDataTest.cpp::"Parameter Rejections"`, section "Pattern Import Not Supported (-9583)". +**Recommendation:** Trust SIMPLNX. Disable Import Pattern Data; use Read HDF5 Dataset to read pattern datasets separately. -**Recommendation:** Trust the corrected behavior. Implementing H5OINA pattern import is a feature request against EbsdLib, not a fix. +**Evidence:** `Parameter Rejections` includes `Pattern Import Not Supported (-9583)`. `vv_work:ReadH5OinaDataFilter/probe_real_file.txt` shows Processed Patterns in the vendor input. Release-source and dependency evidence is in `../provenance/ReadH5OinaDataFilter.md`. --- -## ReadH5OinaDataFilter-D5 +### ReadH5OinaDataFilter-D5 | Field | Value | |---|---| | **Deviation ID** | `ReadH5OinaDataFilter-D5` | | **Filter UUID** | `fad3d47f-f1e1-4429-bc65-5e021be62ba0` | -| **Affected releases** | 7.0.0 through 7.4.1 | -| **Status** | resolved in EbsdLib — requires EbsdLib 3.1.2 — fixed in DREAM3D-NX **7.4.2** | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | -**Symptom:** The `LatticeConstants` ensemble array reported each phase's γ angle as a copy of its β angle. The file's third lattice angle was never read. For a cubic phase, where α = β = γ, the error is invisible; for a hexagonal phase, whose angles are 90/90/120, the reported γ was 90 instead of 120, and the same applies to any monoclinic, triclinic, trigonal or rhombohedral cell whose γ differs from its β. +**Symptom:** The gamma lattice-angle slot contained beta. -**Root cause:** Bug in the trusted parsing boundary. `H5OINAReader::readHeader()` assembled the six lattice constants as `{a, b, c, angles[0], angles[1], angles[1]}`. +**Root cause:** bug. H5OINAReader assembled the angle slots from indices 0, 1, and 1. EbsdLib 3.1.2 uses indices 0, 1, and 2. Copies the third lattice angle into the gamma slot. -**Affected users:** Anyone importing an `.h5oina` file whose phases are not cubic or tetragonal, and who reads `LatticeConstants` downstream. It does not affect orientations, phase indices or crystal-structure symmetry, which come from the Laue group and not from the lattice angles. +**Affected users:** Imports whose phase gamma differs from beta and consumers of LatticeConstants. -**Correct behavior:** The gamma slot receives the third angle. Corrected in EbsdLib 3.1.2. Pinned by `test/ReadH5OinaDataTest.cpp::"Class 1 Analytical Oracle"`, whose hexagonal fixture phase has γ ≠ β specifically so that a gamma slot echoing beta is visible. +**Recommendation:** Trust SIMPLNX. Re-import phase metadata from affected files. -**Recommendation:** Trust the corrected behavior. The correction requires EbsdLib 3.1.2. +**Evidence:** `Class 1 Analytical Oracle` expects lattice angles 90, 90, and 120 degrees for its hexagonal phase. Release-source and dependency evidence is in `../provenance/ReadH5OinaDataFilter.md`. --- -## ReadH5OinaDataFilter-D6 +### ReadH5OinaDataFilter-D6 | Field | Value | |---|---| | **Deviation ID** | `ReadH5OinaDataFilter-D6` | | **Filter UUID** | `fad3d47f-f1e1-4429-bc65-5e021be62ba0` | -| **Affected releases** | 7.0.0 through 7.4.1 | -| **Status** | resolved in EbsdLib — requires EbsdLib 3.1.2 — fixed in DREAM3D-NX **7.4.2** | -| **Breaking change** | **Yes.** The value of a published output array changes for every H5OINA import. | - -**Symptom:** The three angle slots of `LatticeConstants` were reported in radians for H5OINA imports and in degrees for every other EBSD importer. A cubic phase imported from an `.h5oina` file reported `1.5707964, 1.5707964, 1.5707964`, while the same phase imported from a `.ctf` or `.ang` file reported `90, 90, 90`. The array's meaning therefore depended on which file format the phase happened to come from, with nothing in the data to say which. +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | -**Root cause:** Library inconsistency, not a misreading. An H5OINA file stores its lattice angles in radians, and that is correct for the format; `H5OINAReader` copied them through unchanged, while the `.ang` and `.ctf` importers populate the same slots from degree-valued file fields. **The H5OINA files themselves are correct**; what differed was the unit contract at the importer boundary. +**Symptom:** H5OINA lattice angles used radians while the shared phase model used degrees. -**Affected users:** Anyone comparing or combining phase information across file formats, and anyone reading `LatticeConstants` from an H5OINA import while assuming the degrees convention that the rest of DREAM3D-NX uses. +**Root cause:** bug. H5OINAReader copied the file angles without conversion. EbsdLib 3.1.2 converts the three angles to the same degree units used by the ANG and CTF readers. Converts lattice angles to degrees with a double-precision intermediate. -**Correct behavior:** `H5OINAReader` converts the angles from radians to degrees on import, on a double-precision intermediate, so the array carries the same unit no matter which format the phase came from. Corrected in EbsdLib 3.1.2. Pinned by `test/ReadH5OinaDataTest.cpp::"Class 1 Analytical Oracle"` (90, 90, 120 for the hexagonal fixture phase) and `…::"Real AZtec File Readback"` (90, 90, 90 for the production file's titanium-cubic phase). +**Affected users:** All consumers of H5OINA LatticeConstants angle components. -**Release note and migration:** This is a **breaking change to a published output**. The first release that carries it is DREAM3D-NX 7.5.0 with EbsdLib 3.1.2. Every `.dream3d` file written by 7.0.0 through 7.4.1 has radians in components 3, 4 and 5 of `LatticeConstants`, so any saved exemplar, regression baseline or pipeline comparison that reads those components changes value on upgrade. To compare a stored radian value against a new import, multiply it by 180/π. Consumers that compensated by converting H5OINA lattice angles themselves must stop doing so. Nothing else in the import changes unit: `Euler` is still radians and the three lattice dimensions are still unconverted. The user-facing migration note is in `docs/ReadH5OinaDataFilter.md` under "Migration Notes". The archived `H5Oina_Test_Data.dream3d` exemplar has the pre-correction radian values baked in, which is one of the reasons it is no longer used as a comparison target (see `vv/provenance/ReadH5OinaDataFilter.md`). +**Recommendation:** Trust SIMPLNX. Stop any additional radians-to-degrees conversion after import. Multiply saved angle components from affected releases by 180/pi when comparing with new imports; Euler angles remain in radians. -**Recommendation:** Trust the corrected behavior — the degrees convention is the one the rest of the toolkit uses and the one the other importers already produced. +**Evidence:** `Class 1 Analytical Oracle` and `Real AZtec File Readback` check degree-valued lattice angles. `vv_work:ReadH5OinaDataFilter/exemplar_selforacle_check.txt` records the old radian exemplar. Release-source and dependency evidence is in `../provenance/ReadH5OinaDataFilter.md`. --- -## ReadH5OinaDataFilter-D7 +### ReadH5OinaDataFilter-D7 | Field | Value | |---|---| | **Deviation ID** | `ReadH5OinaDataFilter-D7` | | **Filter UUID** | `fad3d47f-f1e1-4429-bc65-5e021be62ba0` | -| **Affected releases** | 7.0.0 through 7.4.1 | -| **Status** | resolved in EbsdLib — requires EbsdLib 3.1.2 — fixed in DREAM3D-NX **7.4.2** | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | -**Symptom:** An `.h5oina` file whose phase group was missing its `Lattice Dimensions` or `Lattice Angles` dataset **crashed the process**. This is empirically demonstrated, not inferred: running the fixture against a build with the corrected filter sources but the pre-correction `H5OINAReader` reports the regression test as `OrientationAnalysis::ReadH5OinaDataFilter: EbsdLib Error Passthrough - Missing Lattice Angles (-9582) (SEGFAULT)` rather than as a failure. The suite log recording that run is listed in `vv/provenance/ReadH5OinaDataFilter.md`. +**Symptom:** Missing lattice datasets caused invalid access during preflight. -**Root cause:** Bug in the trusted parsing boundary. `H5OINAReader::readHeader()` read the two vector datasets while discarding their error codes, then indexed elements 0 through 2 of the resulting vectors, which are empty when the dataset is absent. The `Laue Group` and `Space Group` reads discarded their error codes as well, and a failed phase-group open was not checked at all. +**Root cause:** bug. H5OINAReader ignored failed vector reads and indexed empty lattice vectors. EbsdLib 3.1.2 checks the phase-group open, lattice dimensions, lattice angles, and Laue group with errors `-90030` through `-90033`; Space Group remains optional. Validates required phase data before indexing it. -**Affected users:** Anyone opening a truncated, partially written or otherwise malformed `.h5oina` file. The crash occurs during preflight, so it takes down the application as the file is selected, before any pipeline runs. +**Affected users:** Users opening incomplete or malformed phase metadata. -**Correct behavior:** The four required phase reads are checked and reported with EbsdLib error codes `-90030` through `-90033`, each naming the phase and the dataset; `Space Group` remains optional because the reader only passes it through. The filter surfaces the failure as `-9582` with the file path and the scan name. Corrected in EbsdLib 3.1.2. Pinned by `test/ReadH5OinaDataTest.cpp::"EbsdLib Error Passthrough - Missing Lattice Angles (-9582)"`, which is the fixture that used to crash. +**Recommendation:** Trust SIMPLNX. Repair the file or export it again when preflight reports missing phase data. -**Recommendation:** Trust the corrected behavior. The correction requires EbsdLib 3.1.2. +**Evidence:** `EbsdLib Error Passthrough - Missing Lattice Angles (-9582)` checks first and later scans. `vv_work:ReadH5OinaDataFilter/red_baseline.log` records the earlier crash. Release-source and dependency evidence is in `../provenance/ReadH5OinaDataFilter.md`. --- -## ReadH5OinaDataFilter-D8 +### ReadH5OinaDataFilter-D8 | Field | Value | |---|---| | **Deviation ID** | `ReadH5OinaDataFilter-D8` | | **Filter UUID** | `fad3d47f-f1e1-4429-bc65-5e021be62ba0` | -| **Affected releases** | 7.0.0 through 7.4.1 | -| **Status** | resolved — fixed in DREAM3D-NX **7.4.2** | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | -**Symptom:** A later scan with a phase index above the first scan's phase count wrote past the ensemble arrays and could crash the process. Scans with the same indices but different phase definitions silently used one shared definition for all scan slices. +**Symptom:** Phase groups could exceed ensemble bounds or give different meanings to a shared phase index. -**Root cause:** Bug. The single Ensemble Attribute Matrix that all of the stacked scans share is sized from the first selected scan's phase count, `phases.size() + 1`. The shared ensemble fill in `utilities/IEbsdOemReader.hpp` then runs once per selected scan and writes `crystalStructures[phaseId]`, `materialNames[phaseId]` and `latticeConstants` component `phaseId`, where `phaseId` is the integer in that scan's HDF5 phase group name. Any later scan whose phase index exceeds the first scan's phase count writes past the end of all three arrays. Preflight range-checked those indices for the first selected scan only, and the execute-side `-34972` check inspects the phase *column values*, not the phase *group names*, and runs after the ensemble fill has already happened. +**Root cause:** bug. The ensemble arrays were sized from the first scan and then filled from each scan without checking group indices or compatible definitions. Preflight now reports `-9587`, `-9589`, or `-9590` before those writes. Validates phase indices, counts, and definitions in every selected scan. -**Affected users:** Anyone selecting more than one scan from an `.h5oina` file whose scans differ in their phase lists. Single-scan imports and multi-scan imports of files with one uniform phase list — which is every file in the shipped test data — were never affected, which is why the pre-existing suite could not see it. +**Affected users:** Imports with invalid phase-group indices or differing phase lists between scans. -**Correct behavior:** Preflight validates the phase indices (`-9587`), phase count (`-9589`), and phase definitions (`-9590`) of every selected scan. The definitions must match because the scans become slices of one 3D microstructure and share one Ensemble Attribute Matrix. +**Recommendation:** Trust SIMPLNX. Import scans separately when they describe different phase definitions. -**Recommendation:** Trust the corrected behavior. Re-import any multi-scan file whose phase definitions differ between scans. Import those scans separately if they describe different microstructures. +**Evidence:** `Phase Index Out Of Range rejected (-9587)`, `Scan Phase Count Mismatch rejected (-9589)`, and `Scan Phase Definition Mismatch rejected (-9590)` cover the guards. Release-source and dependency evidence is in `../provenance/ReadH5OinaDataFilter.md`. --- -## ReadH5OinaDataFilter-D9 +### ReadH5OinaDataFilter-D9 | Field | Value | |---|---| | **Deviation ID** | `ReadH5OinaDataFilter-D9` | | **Filter UUID** | `fad3d47f-f1e1-4429-bc65-5e021be62ba0` | -| **Affected releases** | 7.0.0 through 7.4.1 | -| **Status** | resolved — fixed in DREAM3D-NX **7.4.2** | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | -**Symptom:** The **Stacking Order** setting carried by the scan selection had no effect. Choosing *High To Low* produced exactly the same output as *Low To High*, with no error and no warning, so the Z order of a multi-scan stack could not be changed from the parameter that appears to control it. +**Symptom:** The Stacking Order setting had no effect. -**Root cause:** Bug. The scan loop iterated `SelectedScanNames.scanNames` in list order unconditionally and never read `SelectedScanNames.stackingOrder`. The setting reached the algorithm — it is a member of `OEMEbsdScanSelectionParameter::ValueType` — and was simply not consulted. +**Root cause:** bug. The scan loop always used list order and did not read the stacking-order parameter. Low To High now preserves the list; High To Low puts the last selected scan at Z = 0. Reverses the selected scan list for High To Low stacking. -**Affected users:** Anyone importing more than one scan who set the stacking order to *High To Low*. Single-scan imports are unaffected, because the two orders coincide. +**Affected users:** Multi-scan imports with High To Low selected. -**Correct behavior:** *Low To High* stacks the scans in the order they are listed; *High To Low* stacks them in the reverse of that order, so the last selected scan occupies tuple slab 0. Pinned by `test/ReadH5OinaDataTest.cpp::"Stacking Order"`, whose two sections assert cell values that only the corresponding order can produce. +**Recommendation:** Trust SIMPLNX. Check downstream slice selections when repeating an affected pipeline. -**Recommendation:** Trust the corrected behavior. A pipeline saved under 7.0.0 through 7.4.1 with *High To Low* selected now produces a Z-reversed stack relative to what it used to produce, which is what it always asked for; check any such pipeline before re-running it. The two sibling filters `ReadH5OimData` and `ReadH5EspritData` still ignore the same setting — see the V&V report's Follow-ups. +**Evidence:** `Stacking Order` asserts Euler, Band Contrast, and Phase values for both orders. Release-source and dependency evidence is in `../provenance/ReadH5OinaDataFilter.md`. --- -## ReadH5OinaDataFilter-D10 +### ReadH5OinaDataFilter-D10 | Field | Value | |---|---| | **Deviation ID** | `ReadH5OinaDataFilter-D10` | | **Filter UUID** | `fad3d47f-f1e1-4429-bc65-5e021be62ba0` | -| **Affected releases** | 7.0.0 through 7.4.1 | -| **Status** | resolved in EbsdLib — requires EbsdLib 3.1.2 — fixed in DREAM3D-NX **7.4.2** | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | -**Symptom:** Every failure reported out of `H5OINAReader` reached the user with a blank `Message:` field. A malformed `.h5oina` produced an error code and an empty explanation, so nothing in the message said which dataset, phase or scan was at fault. +**Symptom:** Reader failures could have empty explanations or lose the specific inner error. -**Root cause:** Bug in the trusted parsing boundary. Ten failure paths in `H5OINAReader` were written as `std::string str; std::stringstream ss(str); ss << …; setErrorMessage(str);`. `std::stringstream(str)` copies `str` into the stream's own buffer, so everything composed went into that copy and the still-empty original was handed to `setErrorMessage()`. A related defect sat one level up: `readFile()` replaced whatever `readHeader()` or `readData()` had set with a generic "could not read header" / "could not read data", discarding the specific reason even where one had been composed correctly. +**Root cause:** bug. Ten H5OINAReader paths composed text in a stream but stored the unchanged input string. readFile also replaced detailed header and data errors. EbsdLib 3.1.2 preserves the composed text and inner context. Stores the composed error text and includes the inner failure in wrapper messages. -**Affected users:** Anyone who hit any `H5OINAReader` error — a malformed file, a missing dataset, an absent scan. The filter's own `-8970` and `-9582` messages name the file and the scan, so the file was identifiable; the reason was not. +**Affected users:** Users who encounter H5OINA reader failures. -**Correct behavior:** Each of the ten sites composes into a stream of its own and reports that stream's contents, and `readFile()`'s wrappers name the scan and append the inner message rather than replacing it. Corrected in EbsdLib 3.1.2. +**Recommendation:** Trust SIMPLNX. Use the reported file, scan, and dataset context to correct the input. -**Recommendation:** Trust the corrected behavior. The correction requires EbsdLib 3.1.2. +**Evidence:** `EbsdLib Error Passthrough - Missing Data Column (-8970)` and `EbsdLib Error Passthrough - Missing Lattice Angles (-9582)` check error propagation and context. Release-source and dependency evidence is in `../provenance/ReadH5OinaDataFilter.md`. --- -## ReadH5OinaDataFilter-D11 +### ReadH5OinaDataFilter-D11 | Field | Value | |---|---| | **Deviation ID** | `ReadH5OinaDataFilter-D11` | | **Filter UUID** | `fad3d47f-f1e1-4429-bc65-5e021be62ba0` | -| **Affected releases** | 7.0.0 through 7.4.1 | -| **Status** | resolved in EbsdLib — requires EbsdLib 3.1.2 — fixed in DREAM3D-NX **7.4.2** | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | -**Symptom:** Two defects in one code path. A scan whose header declared zero rows was rejected with error code `-90301` recorded on the reader but `-301` handed back as the return value, so a caller reporting the return value and a caller reading `getErrorCode()` disagreed about what had happened. A scan whose header declared a **negative** column count was not rejected at all: the count was widened to `size_t`, producing an enormous allocation request. +**Symptom:** Invalid cell counts could become unsigned dimensions; zero columns returned inconsistent error codes. -**Root cause:** Bug in the trusted parsing boundary. `H5OINAReader::readData()` validated the row count only, and its rejection returned a literal that did not match the code it had just set. +**Root cause:** bug. H5OINAReader widened counts before checking them. With positive rows and zero columns, it stored `-90301` but returned `-301`; zero rows instead returned `-200`. EbsdLib 3.1.2 checks both signed counts. Filter preflight independently rejects invalid geometry counts with `-9584`. Validates both signed cell counts before conversion and returns the stored reader error code. -**Affected users:** Anyone opening a truncated or otherwise malformed `.h5oina` file. DREAM3D-NX reports the code it receives, so the mismatched value was the one the user saw. +**Affected users:** Users opening files with zero or negative cell counts; direct H5OINAReader callers also receive the corrected error code. -**Correct behavior:** Both the row and the column count are validated and the rejection returns the code it sets. Corrected in EbsdLib 3.1.2. The filter rejects the same shape earlier and independently: `-9584` rejects `X Cells` or `Y Cells` below 1 at preflight, which is pinned by `test/ReadH5OinaDataTest.cpp::"Invalid Cell Counts rejected (-9584)"`, including the negative case. +**Recommendation:** Trust SIMPLNX. Supply positive X Cells and Y Cells. -**Recommendation:** Trust the corrected behavior. The filter's `-9584` guard fires first for a file selected through DREAM3D-NX, so this entry matters to other `H5OINAReader` callers. +**Evidence:** `Invalid Cell Counts rejected (-9584)` checks zero X, zero Y, and negative X. EbsdLib release source establishes the return-code correction. Release-source and dependency evidence is in `../provenance/ReadH5OinaDataFilter.md`. --- -## ReadH5OinaDataFilter-D12 +### ReadH5OinaDataFilter-D12 | Field | Value | |---|---| | **Deviation ID** | `ReadH5OinaDataFilter-D12` | | **Filter UUID** | `fad3d47f-f1e1-4429-bc65-5e021be62ba0` | -| **Affected releases** | 7.0.0 through 7.4.1 | -| **Status** | resolved in EbsdLib — requires EbsdLib 3.1.2 — fixed in DREAM3D-NX **7.4.2** | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | -**Symptom:** A non-numeric or noncanonical phase-group name caused `std::stoi()` to throw during preflight. The exception stopped the import and left the opened HDF5 phase group unclosed. +**Symptom:** A non-numeric phase-group name caused an exception after opening the HDF5 group. -**Root cause:** Bug. `H5OINAReader::readHeader()` passed each HDF5 group name directly to `std::stoi()` after it opened the group. The code did not validate that the complete name was a positive integer. +**Root cause:** bug. H5OINAReader called std::stoi on the group name after opening it. An exception bypassed cleanup. EbsdLib 3.1.2 returns `-90034` for noncanonical names; the filter forwards the failure as `-9582`. Validates the complete positive integer name before opening the group. -**Affected users:** Users with a malformed or manually edited H5OINA file whose phase group is not named with a canonical positive integer. +**Affected users:** Users with malformed or manually edited phase-group names. -**Correct behavior:** EbsdLib parses the complete group name before it opens the group. It returns `-90034` for a noncanonical name. The filter reports the error through `-9582` and identifies the file, scan, and group name. +**Recommendation:** Trust SIMPLNX. Use canonical positive integer phase-group names or export the file again. -**Recommendation:** Trust the corrected behavior. Correct the phase-group names or export the file again from AZtec. +**Evidence:** `Invalid Phase Group Name rejected (-9582)` uses the name phase-one and checks the file and group context. Release-source and dependency evidence is in `../provenance/ReadH5OinaDataFilter.md`. --- -## ReadH5OinaDataFilter-D13 +### ReadH5OinaDataFilter-D13 | Field | Value | |---|---| | **Deviation ID** | `ReadH5OinaDataFilter-D13` | | **Filter UUID** | `fad3d47f-f1e1-4429-bc65-5e021be62ba0` | -| **Affected releases** | 7.0.0 through 7.4.1 | -| **Status** | resolved — fixed in DREAM3D-NX **7.4.2** | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | -**Symptom:** The filter accepted non-positive or non-finite X, Y, or Z spacing and created an invalid Image Geometry. +**Symptom:** Non-positive or non-finite spacing could create invalid geometry. -**Root cause:** Bug. Preflight checked only whether Z spacing was less than or equal to zero. It did not reject NaN, and it did not validate the X and Y step values from the file. +**Root cause:** bug. Preflight tested only whether Z spacing was less than or equal to zero. It accepted NaN Z and unchecked X/Y step values. It now reports `-9580` for Z and `-9591` for scan spacing. Requires finite, positive X, Y, and Z spacing. -**Affected users:** Users with malformed H5OINA step values or a non-finite Z Spacing parameter. +**Affected users:** Imports with invalid scan step values or a non-finite Z Spacing parameter. -**Correct behavior:** Preflight requires finite, positive X, Y, and Z spacing. It reports `-9580` for Z spacing and `-9591` for scan spacing, with the actual values and file context. +**Recommendation:** Trust SIMPLNX. Correct the spacing values before import. -**Recommendation:** Trust the corrected behavior. Correct the spacing values or export the file again from AZtec. +**Evidence:** `Parameter Rejections` and `Invalid Scan Spacing rejected (-9591)` check zero, negative, and non-finite values. Release-source and dependency evidence is in `../provenance/ReadH5OinaDataFilter.md`. --- -## ReadH5OinaDataFilter-D14 +### ReadH5OinaDataFilter-D14 | Field | Value | |---|---| | **Deviation ID** | `ReadH5OinaDataFilter-D14` | | **Filter UUID** | `fad3d47f-f1e1-4429-bc65-5e021be62ba0` | -| **Affected releases** | 7.0.0 through 7.4.2 | -| **Status** | resolved in PR #1781 (2026-10-14) | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | The DREAM3D-NX release after v7.4.2 | + +**Symptom:** EDAX alignment skipped Hexagonal-Low phases. + +**Root cause:** bug. The alignment condition accepted only Hexagonal-High. The correction depends on the hexagonal lattice and also applies to Hexagonal-Low. The shared IsHexagonalCrystalStructure predicate now accepts both. Applies pi/6 to both Hexagonal-High and Hexagonal-Low phases. + +**Affected users:** Imports with Hexagonal-Low phases and EDAX alignment enabled. + +**Recommendation:** Trust SIMPLNX. Re-import Hexagonal-Low data when EDAX alignment is required. + +**Evidence:** `Conversion Option Combinations` checks both phase types and alignment states. `Multi-Scan Hexagonal Alignment` checks both hexagonal classes in each scan. PR #1781 (`84c66066a`) supplies this correction. Release-source and dependency evidence is in `../provenance/ReadH5OinaDataFilter.md`. + +--- + +### ReadH5OinaDataFilter-D15 + +| Field | Value | +|---|---| +| **Deviation ID** | `ReadH5OinaDataFilter-D15` | +| **Filter UUID** | `fad3d47f-f1e1-4429-bc65-5e021be62ba0` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | + +**Symptom:** Different scan grids could cause incomplete copies or reads beyond a scan buffer. + +**Root cause:** bug. Output dimensions came from the first scan, but later reader buffers could describe a different grid. Preflight now compares every later scan with the first scan. Rejects incompatible scan dimensions or step sizes with `-9585`. + +**Affected users:** Users selecting scans with different dimensions or spacing. + +**Recommendation:** Trust SIMPLNX. Import incompatible grids separately. + +**Evidence:** `Scan Header Mismatch rejected (-9585)` checks different cell counts and different step sizes. Release-source and dependency evidence is in `../provenance/ReadH5OinaDataFilter.md`. + +--- + +### ReadH5OinaDataFilter-D16 + +| Field | Value | +|---|---| +| **Deviation ID** | `ReadH5OinaDataFilter-D16` | +| **Filter UUID** | `fad3d47f-f1e1-4429-bc65-5e021be62ba0` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | + +**Symptom:** A missing later scan passed preflight and failed after earlier scans had been copied. + +**Root cause:** bug. Preflight read only the first selected header. It did not check later names against the file scan list, so failure left partially populated output arrays. Checks every selected scan name before execution and reports `-9586`. + +**Affected users:** Users with a missing scan name after a valid first selection. + +**Recommendation:** Trust SIMPLNX. Select names that exist in the input file. + +**Evidence:** `Missing Scan Name rejected (-9586)` checks missing first and second scan names. Release-source and dependency evidence is in `../provenance/ReadH5OinaDataFilter.md`. + +--- + +### ReadH5OinaDataFilter-D17 + +| Field | Value | +|---|---| +| **Deviation ID** | `ReadH5OinaDataFilter-D17` | +| **Filter UUID** | `fad3d47f-f1e1-4429-bc65-5e021be62ba0` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | -**Symptom:** With "Convert Hexagonal X-Axis to EDAX Standard" on, only scan points whose phase mapped to `Hexagonal_High` (6/mmm) received the pi/6 shift on phi2. Points whose phase mapped to `Hexagonal_Low` (6/m) were left unshifted, so a 6/m phase imported from an `.h5oina` file stayed in the Oxford/HKL x-axis convention while every 6/mmm phase in the same file was moved to the EDAX/TSL convention. Legacy DREAM3D 6.5 had no H5OINA reader, so there is no legacy comparison; its `.ctf` reader had the same 6/mmm-only limitation (see `ReadCtfDataFilter-D5`). +**Symptom:** Dataset lengths inconsistent with the header could cause out-of-bounds reads or discarded values. -**Root cause:** The Oxford/HKL versus EDAX/TSL x-axis convention depends on the hexagonal lattice, not on the Laue class, so the correction also applies to `Hexagonal_Low` (6/m). +**Root cause:** bug. The reader allocated from dataset extents while the filter copied counts from the header. The filter now has an extent guard (`-34971`); the current reader rejects the short-data fixture earlier and the filter reports `-8970`. Rejects dataset extents that differ from the required point and component counts. -**Affected users:** Users importing 6/m phases with EDAX hexagonal alignment enabled. +**Affected users:** Users with incomplete or inconsistent H5OINA Data datasets. -**Correct behavior:** Both hexagonal Laue classes receive pi/6 radians on phi2 for either phase output type and in each selected scan. Cubic and unindexed points remain unchanged; trigonal phases are outside this correction. Covered by `test/ReadH5OinaDataTest.cpp::"Conversion Option Combinations"` and `…::"Multi-Scan Hexagonal Alignment"`. +**Recommendation:** Trust SIMPLNX. Repair the dataset extents or export the file again. -**Recommendation:** Re-import 6/m data when EDAX alignment is required. +**Evidence:** `Dataset Extent Mismatch rejected (-8970)` asserts reader code `-101100` and the Band Contrast dataset name. `vv_work:ReadH5OinaDataFilter/mutation_table.md` records the earlier filter-guard check. Release-source and dependency evidence is in `../provenance/ReadH5OinaDataFilter.md`. --- -## Malformed-input rejections +### ReadH5OinaDataFilter-D18 + +| Field | Value | +|---|---| +| **Deviation ID** | `ReadH5OinaDataFilter-D18` | +| **Filter UUID** | `fad3d47f-f1e1-4429-bc65-5e021be62ba0` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | + +**Symptom:** An out-of-range cell Phase value could index beyond CrystalStructures. + +**Root cause:** bug. The alignment loop indexed CrystalStructures directly with an unchecked phase byte. The current copy rejects values outside zero through the declared phase count with `-34972`. Validates every cell Phase value before storing it or using it for alignment. -These are not behavioral deviations on well-formed files — every one of them is a rejection path that stands where 7.0.0 through 7.4.1 performed an out-of-range read, an out-of-range write, or produced a silently useless output. They are listed here so a reader auditing the error-code series has one place to find them. The multi-scan phase-group cases are the exception and are recorded as D8, because they are reachable from a well-formed file. +**Affected users:** Users with cell Phase values above the declared phase count. -| Code | Rejects | Previously | -|---|---|---| -| `-9584` | `X Cells` or `Y Cells` below 1 in the first selected scan | The counts were cast to `usize` unchecked, producing a zero-sized geometry, or an enormous one from a negative count | -| `-9585` | A selected scan whose grid or step sizes differ from the first selected scan's | Only the first scan's header was ever read; the copy then spanned the later scan's reader buffers using the first scan's point count | -| `-9586` | A selected scan name that is not in the file | Only the first name was checked, at preflight; a bad later name failed part-way through execute with the earlier scans already written into the output arrays and no rollback | -| `-9587` | A phase group of **any** selected scan named outside 1 through N, where N is the first selected scan's phase count | The check covered the first selected scan only, so a group named `7` in a later scan wrote past the end of the ensemble arrays — see D8 | -| `-9589` | A selected scan whose phase-group count differs from the first selected scan's | The ensemble arrays were sized from the first scan alone and filled from every scan — see D8 | -| `-9590` | A selected scan whose phase definitions differ from the first selected scan's | One shared Ensemble Attribute Matrix silently kept one scan's phase definitions for every scan slice — see D8 | -| `-9591` | Non-positive or non-finite X or Y spacing | The invalid values were assigned to the Image Geometry — see D13 | -| `-34971` | A `Data` dataset whose extent disagrees with the header's cell counts, in either direction | The reader sizes its buffers to the actual extent while the copy spans the header's point count, reading past the end of those buffers when the dataset is short and silently dropping the surplus rows when it is long | -| `-34972` | A phase value outside `[0, phase count]` | The value indexed `CrystalStructures` unchecked in the alignment loop, reading past the end of the ensemble array | +**Recommendation:** Trust SIMPLNX. Correct the Phase column or the phase definitions. -Error codes `-34970` (null pattern data) and `-9588` (stacking order not applied) are retired: the first with the unreachable execute-side pattern block described in D4, the second with the stacking-order implementation described in D9. +**Evidence:** `Out-of-Range Phase Value rejected (-34972)` checks rejection and the file, scan, and point context. Release-source and dependency evidence is in `../provenance/ReadH5OinaDataFilter.md`. diff --git a/src/Plugins/OrientationAnalysis/vv/deviations/RotateEulerRefFrameFilter.md b/src/Plugins/OrientationAnalysis/vv/deviations/RotateEulerRefFrameFilter.md index 57b4d17e0e..72b6ffe397 100644 --- a/src/Plugins/OrientationAnalysis/vv/deviations/RotateEulerRefFrameFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/deviations/RotateEulerRefFrameFilter.md @@ -1,68 +1,51 @@ -# Deviations from DREAM3D 6.5.171: RotateEulerRefFrameFilter +# Deviations from DREAM.3D v6.5.171: RotateEulerRefFrameFilter -This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent. - -Entries are referenced by stable ID (`RotateEulerRefFrameFilter-D`) from the V&V report and from public migration guidance. The ID is stable across renames; the Filter UUID field is the permanent cross-reference anchor. - -| | | -|---|---| -| SIMPLNX UUID | `0458edcd-3655-4465-adc8-b036d76138b5` | -| Legacy (SIMPL) UUID | `{ef9420b2-8c46-55f3-8ae4-f53790639de4}` | -| Comparison fixture | 6 axis/angle cases × 12 orientations, shared CSV input (`Code_Review/RotateEulerRefFrame/`) | -| Comparison date | 2026-07-03 | - -**Headline: no deviations.** DREAM3D 6.5.171 and SIMPLNX agree to within 7.2e-7 rad (float32 ULP level, wrap-aware) on every comparison case, and both independently match the Class 1 analytical oracle. The entries below are **non-deviations** — differences in representation, precision, or invalid-input handling only — recorded so future engineers do not re-discover them. +Part 1 lists the invalid-axis handling difference. Part 2 lists the SIMPLNX store-access defect. Both parts use the stable `RotateEulerRefFrameFilter-D` sequence. --- -## Non-deviation N1 — degree→radian conversion precision - -| Field | Value | -|---|---| -| **ID** | `RotateEulerRefFrameFilter-N1` (non-deviation) | -| **Filter UUID** | `0458edcd-3655-4465-adc8-b036d76138b5` | -| **Status** | informational | - -**Symptom:** None user-visible. Maximum observed output difference between versions is 7.2e-7 rad across all comparison cases. +## Part 1 — Differences from DREAM.3D v6.5.171 -**Root cause:** Precision. Legacy converts the rotation angle degrees→radians in `float` (`float rotAngle = m_RotationAngle * k_Pi / 180.0`, `RotateEulerRefFrame.cpp` line ~215) before handing it to the double-precision orientation transforms; SIMPLNX performs the conversion in `double` (`Algorithms/RotateEulerRefFrame.cpp`, kernel setup). Both then run identical double-precision math. SIMPLNX lands marginally closer to the double-precision oracle (max 2.3e-7 vs legacy 8.1e-7). - -**Affected users:** None at float32 storage precision. - -**Recommendation:** Either acceptable within tolerance 1e-5 rad. - ---- - -## Non-deviation N2 — 0 vs 2π canonical representation at the wrap boundary +### RotateEulerRefFrameFilter-D1 | Field | Value | |---|---| -| **ID** | `RotateEulerRefFrameFilter-N2` (non-deviation) | +| **Deviation ID** | `RotateEulerRefFrameFilter-D1` | | **Filter UUID** | `0458edcd-3655-4465-adc8-b036d76138b5` | -| **Status** | informational | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | + +**Symptom:** A zero rotation axis passed validation although it has no defined direction. SIMPLNX now rejects it before changing the Euler array. -**Symptom:** For an input orientation whose rotated `phi1` lands exactly on the 0/2π boundary — observed for euler (π/2, π/4, ¾π) rotated 90° about Z, where `phi1' = 0` exactly — 6.5.171 outputs `6.2831855` (2π) while SIMPLNX outputs `~6e-17` (0). These represent the same angle. +**Root cause:** Bug. The filter did not validate the axis magnitude. DREAM3D-NX v7.4.1 divided the zero vector by its magnitude and could write NaN values. DREAM.3D v6.5.171 also accepted the invalid axis, but its float `MatrixMath::Normalize3x1()` returned without division when the magnitude was zero. That helper leaves the zero components unchanged. Current `RotateEulerRefFrameFilter.cpp` rejects magnitudes below float32 epsilon with `-96200`; `Algorithms/RotateEulerRefFrame.cpp` repeats the check with `-67050` for direct calls. -**Root cause:** Precision (representation). The N1 float conversion places legacy's intermediate `phi1'` at −ε, which om2eu canonicalizes by adding 2π; SIMPLNX's double conversion places it at +6e-17, which needs no wrap. Element-wise comparison without wrap awareness reports a spurious 2π difference for such boundary values. +**Affected users:** Users whose pipelines supply a zero or very small rotation axis. The current guard also rejects nonzero magnitudes below float32 epsilon. -**Affected users:** Anyone diffing euler arrays element-wise between versions (e.g., regression scripts). Orientation-space comparisons are unaffected. +**Recommendation:** Trust SIMPLNX. Supply a nonzero axis of sufficient magnitude; the filter normalizes it before rotation. -**Recommendation:** Either acceptable — compare euler angles modulo 2π. +**Evidence:** PR #1654 (`11a7acdae`) added both checks. Source at every `upstream/release/v7.0.0` through `upstream/release/v7.4.1` omits them; `upstream/release/v7.4.2` contains them. DREAM.3D tag `v6.5.171`, `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/RotateEulerRefFrame.cpp`, omits axis validation. SIMPL `0afaa6854`, selected at or before that tag date, has the zero-denominator return in `Source/SIMPLib/Math/MatrixMath.cpp`. `Zero-Length Axis Fails Preflight` asserts rejection without checking the error code. The earlier informational label `RotateEulerRefFrameFilter-N3` refers to this same input-validation difference. --- -## Non-deviation N3 — zero-length rotation axis handling (SIMPLNX guard added 2026-07-03) +## Part 2 — SIMPLNX defects and behavior changes + +### RotateEulerRefFrameFilter-D2 | Field | Value | |---|---| -| **ID** | `RotateEulerRefFrameFilter-N3` (non-deviation; deliberate input-validation difference) | +| **Deviation ID** | `RotateEulerRefFrameFilter-D2` | | **Filter UUID** | `0458edcd-3655-4465-adc8-b036d76138b5` | -| **Status** | active; SIMPLNX-side fix ships in DREAM3D-NX **7.4.2** | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | + +**Symptom:** Parallel Euler rotation could access a nonresident store from multiple workers without the required residency check. -**Symptom:** Given rotation axis (0,0,0), 6.5.171 silently fills the entire euler array with NaN (unguarded `axis/|axis|` division). SIMPLNX fails preflight with error `-96200` and a descriptive message. +**Root cause:** Bug. `ParallelDataAlgorithm` dispatched workers that read and wrote `Float32Array::operator[]` without first requiring the Euler array to be resident. DREAM3D-NX v7.4.2 adds `requireArraysInMemory()` before dispatch. The current algorithm uses a serial read-modify-write loop with buffers of at most 65,536 tuples. DREAM.3D v6.5.171 uses a resident raw float array and does not have the nonresident store path. -**Root cause:** Bug (input-validation gap) in 6.5.171, closed on the SIMPLNX side during algorithm review. Not an output deviation for any valid input — outputs are identical whenever the axis is non-zero. Pinned by test `RotateEulerRefFrameFilter: Zero-Length Axis Fails Preflight`. +**Affected users:** Users who supply nonresident Euler arrays to the affected SIMPLNX implementation with parallel execution enabled. -**Affected users:** Users with malformed pipelines: legacy destroys their data silently; SIMPLNX refuses to run. +**Recommendation:** Trust SIMPLNX. DREAM3D-NX v7.4.2 applies the residency safeguard; the current buffered implementation avoids concurrent store access. -**Recommendation:** Trust SIMPLNX. No legacy patch proposed — the legacy codebase is patched only for demonstrably wrong output on *valid* input. +**Evidence:** PR #1654 (`11a7acdae`) records the unsafe worker access and adds `requireArraysInMemory()`. All inspected release sources from `upstream/release/v7.0.0` through `upstream/release/v7.4.1` lack that call; `upstream/release/v7.4.2` contains it. PR #1568 (`4ec21e93f`) replaces the workers with serial buffered traversal. The tagged DREAM.3D source passes a raw `float*` into disjoint worker ranges. Current tests check rotation values; they do not reproduce the earlier concurrent-access failure. diff --git a/src/Plugins/OrientationAnalysis/vv/deviations/WritePoleFigureFilter.md b/src/Plugins/OrientationAnalysis/vv/deviations/WritePoleFigureFilter.md index 81ccef1c81..af67104303 100644 --- a/src/Plugins/OrientationAnalysis/vv/deviations/WritePoleFigureFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/deviations/WritePoleFigureFilter.md @@ -1,14 +1,10 @@ -# Deviations from DREAM3D 6.5.171: WritePoleFigureFilter +# Deviations from DREAM.3D v6.5.171: WritePoleFigureFilter -This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent. +Part 1 lists rendering and output differences (D1–D5). Part 2 lists fixed SIMPLNX defects (D6–D9). Both parts use the stable `WritePoleFigureFilter-D` sequence. -Entries are referenced by stable ID (`WritePoleFigureFilter-D`) from the V&V report and from public migration guidance. The ID is stable across renames; the Filter UUID field is the permanent cross-reference anchor. +## Part 1 — Differences from DREAM.3D v6.5.171 -All entries below were established by an expert (Class 5) side-by-side review of pole figures rendered from the same 502 hex-Ti orientations through DREAM3D 6.5.171, DREAM3D 6.5.172, and SIMPLNX (EbsdLib 3.1.0). 6.5.171 and 6.5.172 outputs are byte-identical. The pole-figure **data** (pole positions, intensity distribution, color-intensity mapping) is visually identical across all three; the only differences are the cosmetic / labeling / rendering items below. None is a correctness defect. - ---- - -## WritePoleFigureFilter-D1 +### WritePoleFigureFilter-D1 | Field | Value | |---|---| @@ -16,17 +12,19 @@ All entries below were established by an expert (Class 5) side-by-side review of | **Filter UUID** | `00cbb97e-a5c2-43e6-9a35-17a0f9ce26ed` | | **Status** | active | -**Symptom:** The in-plane axis annotations on each pole figure differ: legacy DREAM3D labels them **`X` / `Y`**; SIMPLNX labels them **`A1` / `A2`**. +**Symptom:** Sample-axis labels are X/Y in DREAM.3D v6.5.171 and A1/A2 in SIMPLNX. + +**Root cause:** Algorithmic choice. `WritePoleFigure.cpp` supplies A1, A2 and A3 to the EbsdLib compositor. The labels identify the sample axes. -**Root cause:** Algorithmic choice (cosmetic relabeling in the EbsdLib 3.1.0 pole-figure chrome). The axes denote the same sample-frame directions; only the text label changed. +**Affected users:** Users who compare axis annotations between applications. -**Affected users:** Anyone visually comparing a SIMPLNX pole figure image to a legacy one. No effect on data or pole positions. +**Recommendation:** Trust SIMPLNX. The annotation change does not change the selected sample directions. -**Recommendation:** Trust SIMPLNX. Purely a label change; the axes are the same sample directions. +**Evidence:** The DREAM.3D v6.5.171 source writes X and Y with libharu. Current `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/WritePoleFigure.cpp` sets `config.axisNames`. The visual comparison is recorded in `vv_work:WritePoleFigureFilter/README.md`. --- -## WritePoleFigureFilter-D2 +### WritePoleFigureFilter-D2 | Field | Value | |---|---| @@ -34,17 +32,19 @@ All entries below were established by an expert (Class 5) side-by-side review of | **Filter UUID** | `00cbb97e-a5c2-43e6-9a35-17a0f9ce26ed` | | **Status** | active | -**Symptom:** Text in the rendered image (title, labels, legend, color-bar numbers) is drawn in a slightly different font / metrics than legacy. +**Symptom:** Title, label and legend fonts have different shapes and spacing. -**Root cause:** Library. Legacy rasterizes text into a PDF via libharu (Helvetica); SIMPLNX rasterizes via EbsdLib's `canvas_ity` text renderer into a raster image. Different text stack → different glyph shapes and spacing. +**Root cause:** Library. DREAM.3D v6.5.171 uses libharu Helvetica. SIMPLNX renders through the EbsdLib raster compositor, with canvas-based text metrics. -**Affected users:** Anyone doing a pixel-level or visual font comparison. No effect on data, pole positions, or numeric legend values. +**Affected users:** Users who compare image typography or require a matching publication layout. -**Recommendation:** Either acceptable. The text content is the same; only the typeface/metrics differ. This is inherent to the PDF→raster output-medium change (see report Algorithm Relationship: Rewrite). +**Recommendation:** Either rendering is acceptable for typography. Font differences do not define a numerical tolerance for pole positions or intensities. + +**Evidence:** The tagged legacy source selects Helvetica. Current `WritePoleFigureFilter.cpp::GetXCharWidth` uses Lato Bold and canvas_ity. The render comparison is recorded in `vv_work:WritePoleFigureFilter/README.md`. --- -## WritePoleFigureFilter-D3 +### WritePoleFigureFilter-D3 | Field | Value | |---|---| @@ -52,17 +52,19 @@ All entries below were established by an expert (Class 5) side-by-side review of | **Filter UUID** | `00cbb97e-a5c2-43e6-9a35-17a0f9ce26ed` | | **Status** | active | -**Symptom:** The per-pole-figure title labels (the Miller-index family printed above each of the three figures) differ between legacy and SIMPLNX for hexagonal/trigonal phases. +**Symptom:** The displayed family labels for hexagonal and trigonal phases use a different convention. + +**Root cause:** Algorithmic choice. The EbsdLib renderer selects the family-label strings. The hexagonal basis parameter also controls pole positions and must be held constant when comparing figures. -**Root cause:** Algorithmic choice / library (label convention). The labels name **symmetrically equivalent** plane/direction families (e.g. the two basal families), so the labeled families are crystallographically equivalent and the rendered pole figure is the same. Only the chosen representative label string differs. +**Affected users:** Users who read hexagonal or trigonal family labels or compare plots made with different basis conventions. -**Affected users:** Hexagonal/trigonal users reading the family label text. No effect on the plotted poles. +**Recommendation:** Trust SIMPLNX with the basis convention that matches the input Euler angles. Compare the physical families rather than label text alone. -**Recommendation:** Trust SIMPLNX. The labels denote symmetry-equivalent families; the figures are identical. +**Evidence:** The 2026-07-08 visual review of hexagonal images records symmetry-equivalent family labels in `vv_work:WritePoleFigureFilter/README.md`. Current intensity and composite configurations both receive `HexConvention`; `HexConvention choice reaches algorithm` asserts output differences but does not measure a 30-degree rotation. No trigonal comparison fixture is recorded. --- -## WritePoleFigureFilter-D4 +### WritePoleFigureFilter-D4 | Field | Value | |---|---| @@ -70,17 +72,19 @@ All entries below were established by an expert (Class 5) side-by-side review of | **Filter UUID** | `00cbb97e-a5c2-43e6-9a35-17a0f9ce26ed` | | **Status** | active | -**Symptom:** In **Discrete** pole figures, each plotted orientation is drawn as a small filled **circle** (configurable radius) in SIMPLNX, whereas legacy DREAM3D flipped a single pixel to black per orientation. +**Symptom:** Discrete plots use filled-circle markers in SIMPLNX instead of the single-pixel marks used by DREAM.3D v6.5.171. -**Root cause:** Algorithmic choice — a deliberate rendering improvement introduced with EbsdLib 3.1.0's vector-marker renderer (`GeneratePoleFigureComposite` routes discrete, non-heatmap figures to the marker renderer; the new **Discrete Marker Radius** parameter controls the marker size). The underlying pole positions are unchanged; only the per-point glyph is larger and anti-aliased. +**Root cause:** Algorithmic choice. `GeneratePoleFigureComposite` selects the discrete marker renderer. The filter converts the requested marker radius in pixels to a fraction of image size. -**Affected users:** Users of the Discrete pole-figure mode. Markers are more visible than legacy single pixels (the legacy points were often nearly invisible at typical image sizes). Pole positions are identical. +**Affected users:** Users of Discrete mode who compare marker size and visibility. -**Recommendation:** Trust SIMPLNX. This is an intentional, user-controllable visibility improvement; set **Discrete Marker Radius** to taste. No data change. +**Recommendation:** Trust SIMPLNX. Set Discrete Marker Radius to the required display size; it controls the marker glyph. + +**Evidence:** `Discrete mode and marker radius reach algorithm` compares radii of 1 and 10 pixels and also compares Color with Discrete. `vv_work:WritePoleFigureFilter/README.md` records the visual rendering difference. --- -## WritePoleFigureFilter-D5 +### WritePoleFigureFilter-D5 | Field | Value | |---|---| @@ -88,10 +92,104 @@ All entries below were established by an expert (Class 5) side-by-side review of | **Filter UUID** | `00cbb97e-a5c2-43e6-9a35-17a0f9ce26ed` | | **Status** | active | -**Symptom:** Legacy DREAM3D offered an **Image Format** choice (tif/bmp/png/pdf) for the on-disk pole figure file; SIMPLNX has no such parameter and always writes a **PNG** file. +**Symptom:** DREAM.3D v6.5.171 writes a PDF per phase. SIMPLNX writes PNG files and can retain RGB image geometry and numerical count arrays. + +**Root cause:** Algorithmic choice. The legacy image-format selector is inside disabled code, and execution uses `HPDF_SaveToFile`. SIMPLNX uses `PngWriter::WriteColorImage` and does not convert the obsolete `ImageFormat` key. The rewrite keeps the pole-figure purpose but changes the output container and adds numerical outputs. + +**Affected users:** Users whose downstream tools expect PDF files, or whose imported pipeline contains an obsolete image-format key. + +**Recommendation:** Trust SIMPLNX for PNG and data-array output. Convert PNG externally if a PDF container is required. + +**Evidence:** DREAM.3D tag `v6.5.171`, `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/WritePoleFigure.cpp`, disables the selector with `#if 0`. Current `WritePoleFigureFilter.cpp::FromSIMPLJson` omits ImageFormat. The SIMPL conversion test checks the remaining supported arguments. + +--- + +## Part 2 — SIMPLNX defects and behavior changes + +### WritePoleFigureFilter-D6 + +| Field | Value | +|---|---| +| **Deviation ID** | `WritePoleFigureFilter-D6` | +| **Filter UUID** | `00cbb97e-a5c2-43e6-9a35-17a0f9ce26ed` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1 for the image-size omission. DREAM.3D v6.5.171 was affected by that omission. No released version had the marker-radius omission. | +| **Fixed in** | DREAM3D-NX v7.4.2 | + +**Symptom:** Invalid image sizes could reach allocation or rendering. During development, a zero image size also reached marker-radius division, and non-positive marker radii were accepted. + +**Root cause:** Bug. Preflight lacked lower-bound checks. It now rejects image sizes below 1 before output calculations and rejects radii below 1 when Discrete mode is selected. DREAM.3D v6.5.171 also omitted an image-size lower bound; it had no marker-radius parameter. + +**Affected users:** Users who supply zero or negative image sizes. No released version exposed the missing marker-radius validation. + +**Recommendation:** Trust SIMPLNX in DREAM3D-NX v7.4.2 or later. Use an image size of at least 1 and a discrete marker radius of at least 1. -**Root cause:** Algorithmic choice (API simplification). The SIMPLNX rewrite renders through EbsdLib's raster compositor and writes the composite with `PngWriter`; the legacy multi-format option was not carried over. The legacy `ImageFormat` key is dropped during SIMPL pipeline conversion. +**Evidence:** PR #1647 (`64f5f27cb`) adds guards `-680002` and `-680003`. Direct inspection of `WritePoleFigureFilter.cpp` on `upstream/release/v7.0.0` through `upstream/release/v7.4.1` finds no image-size guard; `upstream/release/v7.4.2` has both guards. The legacy `dataCheck()` has no image-size guard. No current test directly checks these invalid inputs. + +--- + +### WritePoleFigureFilter-D7 + +| Field | Value | +|---|---| +| **Deviation ID** | `WritePoleFigureFilter-D7` | +| **Filter UUID** | `00cbb97e-a5c2-43e6-9a35-17a0f9ce26ed` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | -**Affected users:** Anyone whose legacy pipeline selected a non-PNG output format, or whose downstream tooling expects a `.tif`/`.pdf` file extension. The image content is the same composite; only the container format is fixed to PNG. +**Symptom:** An imported SIMPL pipeline could fail preflight because the output geometry reused an existing input container name. -**Recommendation:** Trust SIMPLNX. PNG is lossless and universally readable; convert externally if another container is required. +**Root cause:** Bug. Conversion mapped the crystal-structure input container to the new output geometry path. The output action then attempted to create a geometry at that existing path. Conversion now keeps the default `PoleFigure` output path. DREAM.3D v6.5.171 writes files and does not create this output geometry. + +**Affected users:** Users who import a legacy pipeline whose crystal-structure array is in an existing data container. + +**Recommendation:** Trust SIMPLNX in DREAM3D-NX v7.4.2 or later. Keep the output geometry path distinct from every input container. + +**Evidence:** PR #1682 (`5eb69f359`) removes the erroneous container mapping. `WritePoleFigureFilter.cpp` contains it in every release source from `upstream/release/v7.0.0` through `upstream/release/v7.4.1`; `upstream/release/v7.4.2` omits it. `SIMPL Backwards Compatibility` checks the default `PoleFigure` path for both input formats. + +--- + +### WritePoleFigureFilter-D8 + +| Field | Value | +|---|---| +| **Deviation ID** | `WritePoleFigureFilter-D8` | +| **Filter UUID** | `00cbb97e-a5c2-43e6-9a35-17a0f9ce26ed` | +| **Status** | active | +| **Affected released versions** | None. DREAM3D-NX v7.0.0 through v7.4.2 were not affected. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | + +**Symptom:** The output example named TIFF, and an image-write error named a TIFF writer, while execution wrote PNG. + +**Root cause:** Bug. The writer changed to PNG before the example extension and error text changed. Both now identify PNG. + +**Affected users:** Users of the development version with PNG output and stale TIFF text. No released DREAM3D-NX version contained this mismatch. + +**Recommendation:** Trust SIMPLNX in DREAM3D-NX v7.4.2 or later. Use the PNG extension for the generated image files. + +**Evidence:** PR #1631 (`ec22ac375`) switches the writer to PNG. PR #1647 (`64f5f27cb`) changes the example extension and error text. Every release source from `upstream/release/v7.0.0` through `upstream/release/v7.4.1` uses TIFF for both output and diagnostics; `upstream/release/v7.4.2` uses PNG for both. DREAM.3D v6.5.171 writes PDF through libharu. + +--- + +### WritePoleFigureFilter-D9 + +| Field | Value | +|---|---| +| **Deviation ID** | `WritePoleFigureFilter-D9` | +| **Filter UUID** | `00cbb97e-a5c2-43e6-9a35-17a0f9ce26ed` | +| **Status** | active | +| **Affected released versions** | None. DREAM3D-NX v7.0.0 through v7.4.2 were not affected. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | + +**Symptom:** New filter instances temporarily selected Discrete instead of Color Intensity. + +**Root cause:** Bug. A development default changed the output mode when the pipeline did not supply the parameter. The default was restored to Color Intensity before release. + +**Affected users:** Users of the temporary development version who omitted Pole Figure Type. No released DREAM3D-NX version contained this default change. + +**Recommendation:** Trust SIMPLNX. Set Pole Figure Type explicitly when the mode is part of a reproducible workflow. + +**Evidence:** The PR #1647 merge record (`64f5f27cb`) documents the temporary default and its correction. Direct source inspection of all ten release versions from `upstream/release/v7.0.0` through `upstream/release/v7.4.2` confirms default 0 (Color Intensity). DREAM.3D v6.5.171 also defaults to Color. The current render tests supply the mode explicitly. + +--- diff --git a/src/Plugins/OrientationAnalysis/vv/provenance/BadDataNeighborOrientationCheckFilter.md b/src/Plugins/OrientationAnalysis/vv/provenance/BadDataNeighborOrientationCheckFilter.md index 1e82528caf..cb4014ce1e 100644 --- a/src/Plugins/OrientationAnalysis/vv/provenance/BadDataNeighborOrientationCheckFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/provenance/BadDataNeighborOrientationCheckFilter.md @@ -1,89 +1,69 @@ -# Exemplar Archive Provenance: `7_bad_data_neighbor_orientation_check.tar.gz` +# Exemplar Archive Provenance: bad_data_neighbor_orientation_check_v2.tar.gz -This sidecar records how the input data archive used by `BadDataNeighborOrientationCheckFilter`'s unit tests was generated. - -**Important:** Unlike most simplnx test archives, this archive contains **INPUT** `.dream3d` files only — the test "exemplar" outputs are hard-coded `std::array` arrays inline in `test/BadDataNeighborOrientationCheckTest.cpp`, NOT stored as `.dream3d` arrays in the archive. No archive re-upload was needed during this V&V cycle; only inline test-source updates and the SIMPLNX algorithm fix were necessary. - ---- +This sidecar records the test-input archive, independent expected masks, release-source checks and legacy comparison for `BadDataNeighborOrientationCheckFilter`. ## Archive identity -| Field | Value | -|-------------------|--------------------------------------------------------------------------------------------------------| -| **Archive** | `7_bad_data_neighbor_orientation_check.tar.gz` | -| **SHA512** | `60089eecfe679466f63ef46839f194f83185a5987f51a0e23b9670e50d967ae49451bcfa43c0d44d6fb12cd55b73d208b36825251842d2b2568ffe521be12fbe` | -| **Used by tests** | `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.X.Y` (27 algorithmic cases) + `Class 4 Invariants Sweep` (DYNAMIC_SECTIONs over 18 fixtures) + `Class 4 Idempotence` (uses Case 4 input). The `2D Image Fixture` test does NOT consume the archive (inline construction). | -| **Generated by** | Nathan Young (algorithm rewrite + initial dataset, PR #1499, merged 2026-02-02). Engineer's design notes are in `bad_data_neighbor_orientation_check_v2/README.md` and `test_design.md` (bundled with the local archive copy). | -| **Generated on** | 2026-01-28 (per the `README.md` last-modified date in the local archive copy) | +| Field | Value | +|---|---| +| **Archive** | `bad_data_neighbor_orientation_check_v2.tar.gz` | +| **SHA512** | `c311d636f56027da8f3b665375005230be83bb9060aed29dd1aada928d7afbce89d7be845626139c19025a89aaf1ac52b099c8efb8b99f246fc0bfad3c4ce128` | +| **Used by tests** | `Case 1.1.1` through `Case 4` (27 fixtures), `Class 4 Invariants Sweep` (18 Case 1 fixtures), and `Class 4 Idempotence` (Case 4 input). | +| **Generated by** | Nathan Young (inferred from PR #1499) | +| **Generated on** | 2026-01-08 to 2026-02-02 (inferred from PR #1499) | +| **Generated at commit** | `ebdce592f` (PR #1499 merge, inferred) | -## How it was generated +PR #1631 (`ec22ac375`) registers this archive name and SHA512 in `src/Plugins/OrientationAnalysis/test/CMakeLists.txt:173`. -The archive contains a hierarchy of **input-only** `.dream3d` files, one per test case, organized by parameter combination: +CI at `01809c236` records passes for 33 ordinary test cases on Linux, macOS and Windows. The current test source adds `Preflight Error - Number of neighbors out of range (-6810)` and retains those 33 cases. The 2D, phase-bounds, tuple-count, neighbor-count and HDF5 fixtures construct their inputs in C++. -``` -bad_data_neighbor_orientation_check/ -├── case_1/case_1_X/case_1_X_Y/case_1_X_Y_input.dream3d (18 cases, 3×3×3 each) -├── case_2/case_2_X/case_2_X_input.dream3d (6 cases, 5×5×5 each) -├── case_3/case_3_X/case_3_X_input.dream3d (2 cases, 5×5×5 each) -└── case_4/case_4_input.dream3d (1 case, 5×5×5) -``` +## How it was generated -Each `case_*_input.dream3d` encodes: -- An ImageGeom (3×3×3 for Case 1, 5×5×5 for Cases 2/3/4) -- A `Quats` `float32` array (4 components per voxel) with hand-chosen orientations using pure φ1-rotations in degrees (Bunge ZXZ Euler `(φ1, 0, 0)`). The misorientation between any two voxels then equals `|Δφ1|` modulo the c-axis-aligned symmetry of the relevant Laue group — a closed-form derivation independent of any DREAM3D version. -- A `Phases` `int32` array (one entry per voxel) with hand-chosen phase indices (1, 2, or 3) -- A `Mask` `uint8` array (one entry per voxel) marking which voxels are initially "good" (`true`) vs "bad" (`false`) -- A `CrystalStructures` `uint32` ensemble array (typically 2 entries: `[UnknownCrystalStructure=999, Cubic_High=1]`) +The 27 synthetic fixtures use hand-selected orientations, phases and initial masks: 18 base cases, eight propagation cases and one mixed-phase case. Per-case CSV files and input pipelines define the data; the design and generation method are in `vv_work:BadDataNeighborOrientationCheckFilter/notes/engineer_test_design.md` and `vv_work:BadDataNeighborOrientationCheckFilter/notes/engineer_archive_README.md`. For the A/B comparison, `vv_work:BadDataNeighborOrientationCheckFilter/scripts/make_fixture_inputs.py` copies the fixture arrays into shared legacy-format inputs. `vv_work:BadDataNeighborOrientationCheckFilter/comparison/archive_audit.json` confirms that all 27 fixture cell arrays match both the CSVs and the original NX inputs. -The choice of orientations and initial mask is designed so that, given specific `(MisorientationTolerance, NumberOfNeighbors)` parameter values, the algorithm's iterative-flipping convergence has a predictable, hand-derivable output. The expected output mask is therefore hand-coded in the test source rather than loaded from the archive. Each Case 1.X.Y has a hand-derived `expectedMask` visualization in the test docstring above the literal array, so the analytical derivation is auditable from the test source alone. +The test inputs contain Quats, Phases and Mask under `Image Geometry/Cell Data`, and Crystal Structures under `Image Geometry/Cell Ensemble Data`. Case 1 uses 3×3×3 images; Cases 2, 3 and 4 use 5×5×5 images. Expected output masks are defined in C++, not loaded from a computed output file. -## Canonical oracle output +The comparison dated 2026-09-17 uses those 27 fixtures and Small IN100. The stock DREAM.3D v6.5.171 application is identified by its bundle version and the runner's `SIMPLib Version 1.2.828.f45085c83` banner in `vv_work:BadDataNeighborOrientationCheckFilter/ReadMe.md` and `vv_work:BadDataNeighborOrientationCheckFilter/scratch/final_surgical_2026_09_17/case_1_1_1_6_5_171.stdout.log`. The NX runner embeds commit `302bc21c3f79b8e3bbd7a148132e8a1f7dc1810a`; its filter, algorithm and test source match the comparison checkout `8b80721c20853d891d5040f9134315af1d93b2b6`. -| DataPath | Source of expected values | -|---------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `/Image Geometry/Cell Data/Mask` (post-filter) | **Class 1 (Analytical)** — hand-derived from input orientations + tolerance comparison + iterative-flipping convergence. Encoded as `std::array expectedMask = {...}` literals inline in each `TEST_CASE` block, with the docstring above each array depicting the same mask in 3×3 grid form. | -| (no specific path) | **Class 4 (Invariant)** — monotonicity + no-degrade invariants, asserted via `ClassFourInvariants::AssertInvariants()` in `Class 4 Invariants Sweep` and `Class 4 Idempotence` tests. | +| Comparison | Matching cases | Different Mask values | +|---|---|---| +| SIMPLNX vs DREAM.3D v6.5.171 | 12 of 28 | 45,229: 288 in the analytical fixtures and 44,941 in Small IN100; all are SIMPLNX good and legacy bad. | +| SIMPLNX vs a local proof version | 28 of 28 | 0 across Mask, Phases, Quats and Crystal Structures. | -The Class 1 expected values are asserted via `REQUIRE(maskStore.getValue(i) == expectedMask[i])` checks (one per voxel × 27 cases = ~729 assertions for the base cases, plus 125-cell checks for Case 4 and Cases 2/3 = several thousand additional assertions). +Phases, Quats and Crystal Structures match across all three binaries and remain unchanged from input. All 27 SIMPLNX fixture masks match the test-source oracle. Small IN100 is a migration comparison, with no independent expected mask. The local proof version combines the loop-bound correction, phase gating, tolerance conversion and a CubicOps correction; its Small IN100 agreement does not isolate each correction's contribution. -## Oracle provenance +Evidence: `vv_work:BadDataNeighborOrientationCheckFilter/comparison/comparison_report.md`, `vv_work:BadDataNeighborOrientationCheckFilter/comparison/raw_diff.txt`, and `vv_work:BadDataNeighborOrientationCheckFilter/comparison/archive_audit.json`. The audit records 336 array checks, equivalent pipeline parameters and unchanged input hashes. `vv_work:BadDataNeighborOrientationCheckFilter/scratch/final_surgical_2026_09_17/runs.json` records 84 successful runner executions with timestamps and binary, pipeline and output hashes. -### Class 1 — Analytical +Release exposure comes from the algorithm source on every fetched `upstream/release/v` branch from v7.0.0 through v7.4.2. The inspected file is `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/BadDataNeighborOrientationCheck.cpp`. -The Class 1 oracle lives in `test_design.md` (in the local archive copy at `bad_data_neighbor_orientation_check_v2/test_design.md`) and is mirrored inline as `expectedMask` literals in `test/BadDataNeighborOrientationCheckTest.cpp`. The engineer's derivation for each case is the input-mask + Quats + Phases configuration, the misorientation tolerance check (strict `<`), and the iterative-flipping convergence rule. Closed-form derivable on paper for every case. +| Defects | Affected DREAM3D-NX releases | Source result and fix | +|---|---|---| +| D1, D2 | v7.0.0 through v7.4.0 | Exclusive level bound and angle test outside the phase condition. The v7.4.1 branch (`68a361753`) has both PR #1499 corrections. | +| D3, D4 | v7.0.0 through v7.4.1 | Float tolerance conversion and unchecked Laue indices. The v7.4.2 branch (`a307946e7`) has both PR #1631 corrections. | +| D5 | v7.0.0 through v7.4.2 | Crystal Structures is read through an unchecked phase index. PR #1568 (`4ec21e93f`) adds the bounds checks; the fix will be in the DREAM3D-NX release after v7.4.2. | -### Class 4 — Invariant +DREAM.3D v6.5.171 was affected by all five defects. Its tagged `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/BadDataNeighborOrientationCheck.cpp` uses float tolerance conversion, an exclusive level bound, angle tests outside the phase condition, and unchecked phase and orientation-operator indices. Its `dataCheck()` checks array types and tuple counts, without validating phase or crystal-structure values. -Two invariants the filter must satisfy for any input configuration: -- **Monotonicity**: the count of `Mask == true` voxels is non-decreasing across one filter run. -- **No-degrade**: no voxel goes from `true` (good) to `false` (bad). +## Canonical oracle output -Both invariants are encoded as a single helper (`namespace ClassFourInvariants` in `BadDataNeighborOrientationCheckTest.cpp`) and asserted from: -- `Class 4 Invariants Sweep` (DYNAMIC_SECTIONs over 18 Case 1.X.Y fixtures, each running the filter and asserting invariants on the resulting mask). -- `Class 4 Idempotence` (runs Case 4 input through the filter twice; second run must produce the same mask as the first). +| DataPath | Source of expected values | +|---|---| +| `/Image Geometry/Cell Data/Mask` | Class 1 hand derivation in `vv_work:BadDataNeighborOrientationCheckFilter/notes/engineer_test_design.md`, encoded in `src/Plugins/OrientationAnalysis/test/BadDataNeighborOrientationCheckTest.cpp`. Cases 1.1.1 through 1.6.3 compare 27 values each with literal arrays; Cases 2.1 through 3.2 require every value to be 1; Case 4 compares 125 values with a literal array. | +| `/Image Geometry/Cell Data/Mask` | Class 4: good voxels remain good and their count does not decrease in the 18-fixture invariant sweep. Case 4 idempotence compares the masks from two consecutive executions. | -### Class 2, 3, 5 +The 3×3×1 constructed fixture expects `[0,1,0,1,1,1,0,1,0]`. The 3×3×5 HDF5 fixture expects the five central-column voxels to become good from one seed at index 40; a neighbor rotated by 30° remains bad at a 1° tolerance. -N/A. This filter delegates misorientation math to `ebsdlib::LaueOps::calculateMisorientation`; the Rowenhorst 2015 paper-based verification of that math is part of EbsdLib's own V&V, not this filter's. This filter consumes that math transitively at the pinned EbsdLib version. +## Oracle provenance (Classes 2, 3, 5 only) -**EbsdLib version pin:** EbsdLib 2.4.1 (commit `5c8c993` on `/Users/mjackson/Workspace9/EbsdLib`). Recorded here for traceability since SIMPLNX's misorientation results are coupled to this pin. +Not applicable. The correctness oracles are Classes 1 and 4. Legacy outputs and the local proof version explain migration differences; they do not define expected masks. ## Second-engineer oracle review -- **Reviewer:** *Pending — recommend Joey Kleingers or another OA-domain engineer review:* - - *The Class 1 hand-derivations in `test_design.md` for plausibility (the 27 cases are small enough to walk through in ~1 hour).* - - *The Class 4 invariant set for completeness — are there other properties this algorithm must satisfy?* - - *The Phase 9 deviation narrative (D1 loop bound + D2 stale `w`) and the determination that the EbsdLib 2.4.1 CubicOps precision improvement is non-observable in this filter's test data.* -- **Date:** *YYYY-MM-DD (pending)* +- **Reviewer:** Michael Jackson, as recorded in the report. +- **Date:** 2026-06-01 ## Regenerated to fix a circular-oracle situation? -**Partially yes** — but the situation was resolved by fixing SIMPLNX, not by regenerating the archive. - -**What happened:** On 2026-05-29 the test-source `expectedMask[13]` values for the 4 Case 1.X.3 fixtures were updated from `0` (matching the engineer's hand-derived oracle in `test_design.md`) to `1` (matching the then-current SIMPLNX output). This was a **circular oracle** — the test was being made to confirm whatever the code produced, regardless of correctness. - -**Root cause:** A SIMPLNX-side bug in the misorientation tolerance computation (float-π precision amplification) caused the 4 boundary-exact Case 1.X.3 fixtures to produce `mask[13] = 1` where the analytical oracle says `0`. The bug was not in the algorithm logic; it was in the tolerance-radians conversion: `numbers::pi_v` is slightly larger than true π, so the float-radian tolerance is ~5e-9 rad larger than `5° × π_true / 180`. Boundary-exact misorientations (those landing at *exactly* the user-supplied tolerance) were inappropriately counted as within-tolerance. - -**Resolution (Phase 6, 2026-05-29):** SIMPLNX's `Algorithms/BadDataNeighborOrientationCheck.cpp` line 41 was changed from `numbers::pi_v` to `numbers::pi_v` (with the variable type also upgraded `float` → `double` so the conversion isn't truncated back to float at assignment). The 4 inline `expectedMask[13]` literals were reverted from `1` back to `0` to match the engineer's analytical oracle. With both changes applied, all 31 tests pass and SIMPLNX is bit-identical to the engineer's `test_design.md` oracle. +The canonical masks remain the analytical C++ expectations. PR #1631 corrects tolerance conversion and registers the v2 input archive. Cases 1.3.3, 1.4.3, 1.5.3 and 1.6.3 keep the center bad. -The archive itself was NOT regenerated. The archive's input data was always correct; the issue was a precision bug in SIMPLNX's consumption of that data and a test that had been bent to track the bug. +`vv_work:BadDataNeighborOrientationCheckFilter/algorithm_review/source_comparison.md` gives the numerical boundary evidence. At 5°, float tolerance conversion gives `0.0872664675116539` radians; double conversion gives `0.087266462599716474`. The stored quaternions produce a double angle of `0.087266463155491913`, slightly above the tolerance. `vv_work:BadDataNeighborOrientationCheckFilter/comparison/legacy_emulator_results.md` isolates 37 phase-gating differences and four tolerance differences in a local proof version; the final compiled proof removes all 41 differences and matches all 27 fixture masks. diff --git a/src/Plugins/OrientationAnalysis/vv/provenance/CAxisSegmentFeaturesFilter.md b/src/Plugins/OrientationAnalysis/vv/provenance/CAxisSegmentFeaturesFilter.md index 7976321a73..594cf191d4 100644 --- a/src/Plugins/OrientationAnalysis/vv/provenance/CAxisSegmentFeaturesFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/provenance/CAxisSegmentFeaturesFilter.md @@ -1,45 +1,67 @@ -# Exemplar Archive Provenance: CAxisSegmentFeaturesFilter (inline fixtures — no archive) +# Exemplar Archive Provenance: segment_features_test_data.tar.gz -This sidecar records the provenance of the oracle data for `CAxisSegmentFeaturesFilter`. There is **no exemplar archive**: all oracle fixtures are built inline in the test source, so the oracle provenance lives in code and in this note. - ---- +This sidecar records the regression archive, independent analytical fixtures and release comparison for `CAxisSegmentFeaturesFilter`. ## Archive identity | Field | Value | |---|---| -| **Archive** | None — fixtures inlined in `test/CAxisSegmentFeaturesTest.cpp` (namespace `AnalyticalFixtures`) | -| **SHA512** | N/A | -| **Used by tests** | All `OrientationAnalysis::CAxisSegmentFeaturesFilter:*` test cases except `SIMPL Backwards Compatibility` (JSON fixtures) and the hand-built preflight-error tests (`-651`/`-652`/`-653`) | -| **Generated by** | Michael Jackson | -| **Generated on** | 2026-07-22 | -| **Generated at commit** | *the V&V commit on `vv/c_axis_segmentation`* | +| **Archive** | `segment_features_test_data.tar.gz` | +| **SHA512** | `317d69384330d40c673f8e1a42df003dede5ac85331b2549e9f45467f7af6b74284f8dad1120427690719ebcd5066830e17031533381cc2cc0cc8622a422b914` | +| **Used by tests** | `OrientationAnalysis::CAxisSegmentFeatures:Face`, `:All`, `:MaskFace` and `:MaskAll` | +| **Generated by** | Michael Jackson (inferred from PR #1466) | +| **Generated on** | — | +| **Generated at commit** | `3d1cc153a` (PR #1466 merge, inferred) | + +PR #1466 first registers this archive name and SHA512 in `src/Plugins/OrientationAnalysis/test/CMakeLists.txt`. The current registration has the same hash. The local commit metadata dates this registration to 2025-11-14; that date records archive registration, not archive generation. ## How it was generated -`AnalyticalFixtures::CreateScaffold(dimX, dimY, dimZ, numEnsembles)` builds a minimal ImageGeom with Cell (Quats, Phases, optional Mask) and CellEnsemble (CrystalStructures) AttributeMatrices; one test builds a RectGridGeom variant directly. Per-cell orientations are pure Bunge (φ1=0, Φ, φ2=0) quaternions `{sin(Φ/2), 0, 0, cos(Φ/2)}` set via `SetPhi()`. All inputs are hand-constructed; no external data and no DREAM3D output (of any version) is consumed. +The regression tests read `segment_features_test_data/segment_features_test_data.dream3d`. They select Quats, Phases and Mask (Y Pos) from `DataContainer/CellData`, and CrystalStructures from `DataContainer/CellEnsembleData`. Four stored label arrays supply regression expectations for face or all-connected neighbors, with or without the mask. Generation authorship and commit above follow the first archive registration. + +The separate analytical fixtures are constructed in `src/Plugins/OrientationAnalysis/test/CAxisSegmentFeaturesTest.cpp`. `AnalyticalFixtures::CreateScaffold()` builds Image geometry, and `QuatFromPhiDeg()` supplies hand-selected pure-Phi orientations. The RectGrid fixture builds unequal X intervals directly. The hidden `Generate Test Data` case writes synthetic inputs; it does not compute canonical labels. + +The legacy comparison inputs come from `vv_work:CAxisSegmentFeaturesFilter/scripts/make_inputs.py`. That script writes shared legacy-format data for ten cases; `vv_work:CAxisSegmentFeaturesFilter/scripts/make_pipelines.py` constructs their pipelines. `vv_work:CAxisSegmentFeaturesFilter/scripts/compare_matrix.py` compares partitions after replacing labels by first-occurrence order. ## Canonical oracle output -The oracle is **Class 1 (Analytical)** with **Class 4 (Invariant)** companions; expected values are hand-derived constants inlined next to the assertions. +Classes 1 and 4 provide the independent correctness expectations in the C++ test source. The archive arrays are regression references. | DataPath | Source of expected values | |---|---| -| `ImageGeometry/CellData/FeatureIds` | Class 1 hand derivation: pure-Phi c-axes make the folded angular distance between cells exactly `min(\|ΦA−ΦB\|, 180°−\|ΦA−ΦB\|)`; expected FeatureIds follow from tolerance + grid adjacency (derivation comment at `QuatFromPhiDeg`) | -| `ImageGeometry/CellFeatureData/Active` | Class 4 invariants: tuple count = numFeatures+1, `Active[0]==0`, all real features active (`CheckActiveArray`) | +| `ImageGeometry/CellData/FeatureIds` | Class 1: pure-Phi folded angles, equal positive phases, mask values and grid adjacency. Eight analytical cases assert exact labels. | +| `ImageGeometry/CellFeatureData/Active` | Class 4: N + 1 tuples for N real features, Active[0] = 0 and every real feature active. | +| `ImageGeometry/CellData/FeatureIds` after randomization | Class 4: unchanged partition, IDs 1 through N without gaps, deterministic relabeling and masked zeros preserved. | +| `DataContainer/CellData/CAxis_FeatureIds_Face` | Archive regression reference; the test also requires 57 Active tuples. | +| `DataContainer/CellData/CAxis_FeatureIds_All` | Archive regression reference; the test also requires 37 Active tuples. | +| `DataContainer/CellData/CAxis_FeatureIds_Mask_Face` | Archive regression reference; the test also requires 31 Active tuples. | +| `DataContainer/CellData/CAxis_FeatureIds_Mask_All` | Archive regression reference; the test also requires 25 Active tuples. | + +For the pure-Phi fixtures, quaternions are `(sin(Phi/2), 0, 0, cos(Phi/2))`. Their c-axes are `(0, ±sin(Phi), cos(Phi))`, and the test angles give folded distance `min(abs(ΔPhi), 180° − abs(ΔPhi))`. The 3×2×2 fixture uses different orientation values along each axis; the phase fixture uses both hexagonal classes. Physical origin and spacing do not enter the adjacency or angle calculation. -The same fixtures drive the 6.5.171 A/B comparison (`vv/comparisons/CAxisSegmentFeaturesFilter/pipelines/make_input.py`). +All 28 ordinary test cases pass in CI at `419f9db35`. The hidden input generator is listed separately in the report. ## Oracle provenance (Classes 2, 3, 5 only) -Not applicable — Classes 1 and 4; the derivation lives in the test source. +Not applicable. Classes 1 and 4 are encoded in the test source. The following release comparison explains migration behavior and does not define the correctness oracle. + +Run against DREAM.3D v6.5.171 on 2026-09-17: 6 of 6 valid shared Image cases match DREAM3D-NX v7.4.2 at the partition level and have equal feature counts. These are TC1 through TC6. Both applications return `-87000` for all-masked TC10. DREAM3D-NX v7.4.2 rejects out-of-range phase TC7 and non-hexagonal TC9, while DREAM.3D v6.5.171 writes segmentations. RectGrid TC8 is a SIMPLNX-only case. + +`vv_work:CAxisSegmentFeaturesFilter/ReadMe.md` identifies the official DREAM.3D v6.5.171 PipelineRunner. `vv_work:CAxisSegmentFeaturesFilter/comparison/comparison_results.md` gives every result; `vv_work:CAxisSegmentFeaturesFilter/comparison/run_matrix.json` records the runner executions. Repeated legacy runs change raw labels while preserving partitions. A local proof version with randomization disabled matches v7.4.2 raw labels on all six valid shared Image cases. + +Release exposure comes from the source at each `upstream/release/v` branch: v7.0.0, v7.0.1, v7.0.2, v7.0.3, v7.2.0, v7.2.1, v7.2.2, v7.4.0, v7.4.1 and v7.4.2. The source paths are `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/CAxisSegmentFeatures.cpp`, `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/CAxisSegmentFeaturesFilter.cpp` and `src/simplnx/Utilities/SegmentFeatures.cpp`. + +Legacy source is the DREAM.3D `v6.5.171` tag, `Source/Plugins/Reconstruction/ReconstructionFilters/CAxisSegmentFeatures.cpp` and `SegmentFeatures.cpp`. SIMPL validation uses commit `0afaa6854`, the last commit before the tag date, in `Source/SIMPLib/DataContainers/DataContainerArray.h`. + +The matrix also compares DREAM3D-NX v7.4.0 and v7.4.1 with v7.4.2. It demonstrates the D1, D4 and D5 corrections and the separate D6 feature-count defect. The detailed mechanism is in `vv_work:CAxisSegmentFeaturesFilter/algorithm_review/source_comparison.md`; the current deviations sidecar supplies release ranges from direct release-source checks. + +The current algorithm differs from the comparison implementation. PR #1568 (`4ec21e93f`, 2026-09-20) replaces flood fill with connected-component labeling and changes validation and slice access. PR #1697 (`a33c370ca`, 2026-09-25) changes progress messaging. The 2026-09-17 result applies to DREAM3D-NX v7.4.2. ## Second-engineer oracle review -- **Reviewer:** pending — recommended at PR review. The pure-Phi Bunge c-axis derivation is sibling-shared with `GroupMicroTextureRegionsFilter` and `ComputeFeatureNeighborCAxisMisalignmentsFilter` Class 1 oracles, which have been through prior V&V cycles. -- **Date:** — -- **Skip reason:** not skipped; deferred to the PR review per the sign-off convention. +- **Reviewer:** Matthew Marine, as recorded in the report sign-off for PR #1685. +- **Date:** 2026-07-25. ## Regenerated to fix a circular-oracle situation? -Yes — this inline-fixture suite **replaces the filter's consumption of `segment_features_test_data.tar.gz`** (which remains in the Data_Archive for the `EBSDSegmentFeatures` tests). That archive's `CAxis_FeatureIds_*` exemplar arrays were generated by running SIMPLNX itself, so they constituted a circular oracle: they encoded the pre-V&V SIMPLNX behavior, including the D1 unvalidated-first-seed defect era, and could not certify correctness. The four retired exemplar tests (`:Face`, `:All`, `:MaskFace`, `:MaskAll`) are superseded by the Class 1 analytical tests, which cover the same parameter cube (face/all neighbors × mask on/off) with independently derived expected output. +PR #1685 replaces the four archive comparisons with independently derived C++ fixtures. PR #1568 restores the four archive regression tests alongside those fixtures. The current CMake registration retains the SHA512 first added by PR #1466. The analytical expected values remain independent of archived labels and filter-generated output. diff --git a/src/Plugins/OrientationAnalysis/vv/provenance/ComputeAvgCAxesFilter.md b/src/Plugins/OrientationAnalysis/vv/provenance/ComputeAvgCAxesFilter.md index fe5cda7181..643acb34bb 100644 --- a/src/Plugins/OrientationAnalysis/vv/provenance/ComputeAvgCAxesFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/provenance/ComputeAvgCAxesFilter.md @@ -1,141 +1,120 @@ # Exemplar Archive Provenance: compute_avg_c_axis.tar.gz -This sidecar records how the test data used by `ComputeAvgCAxesFilter`'s Class 1 (Analytical) + Class 4 (Invariant) unit tests was generated. It is the answer to "where did this hand-built data come from?" - -The test data is published as a tar.gz archive on the GitHub Data_Archive release and consumed by `download_test_data()` in `src/Plugins/OrientationAnalysis/test/CMakeLists.txt`. The dataset is hand-built and reviewed by Michael A. Jackson with assistance from Claude Opus 4.7. MAJ also signed off on the dataset as the canonical Class 1 oracle for this filter. - ---- +This sidecar records the hand-built inputs and the independent Class 1 and Class 4 expectations used by `ComputeAvgCAxesFilter`. ## Archive identity -| Field | Value | -|-------------------|----------------------------------------------------------------------------------------------------------------------------------------------------| -| **Archive** | `compute_avg_c_axis.tar.gz` | -| **SHA512** | `4ee957b4a5e78e1d75e3585016a33de40985c66a8e8d1036b252e5974eb2b3360f34dacdcb51cd1d1ae25bfef2bb638979912cbc4555ff521eb2f42a167155b0` | -| **Used by tests** | `OrientationAnalysis::ComputeAvgCAxesFilter: Class 1 Oracle (hand-built dataset)`| -| | `OrientationAnalysis::ComputeAvgCAxesFilter:No_Hex_Phase` (reuses the Class 1 input, mutates the ensemble to all-non-hex to trigger the -1567 error) | -| **Generated by** | Hand-designed by Michael Jackson with LLM (Claude, Anthropic) assistance — see *Generator attribution* below | -| **Signed off by** | Michael Jackson (BlueQuartz Software), 2026-05-28 — hand-reviewed the input `.dream3d` directly, ran the A/B (SIMPLNX vs DREAM3D 6.5.171), and confirmed the root-cause proof: after applying the surgical fixes to a local build of the legacy source, the legacy output is bit-identical to SIMPLNX across all 8 features | -| **Generated on** | 2026-05-28 (initial publish) | +| Field | Value | +|---|---| +| **Archive** | `compute_avg_c_axis.tar.gz` | +| **SHA512** | `4ee957b4a5e78e1d75e3585016a33de40985c66a8e8d1036b252e5974eb2b3360f34dacdcb51cd1d1ae25bfef2bb638979912cbc4555ff521eb2f42a167155b0` | +| **Used by tests** | `OrientationAnalysis::ComputeAvgCAxesFilter: Class 1 Oracle (hand-built dataset)` and `OrientationAnalysis::ComputeAvgCAxesFilter:No_Hex_Phase` | +| **Generated by** | Michael Jackson, with design and Python-script assistance from Claude Opus 4.7 | +| **Generated on** | 2026-05-28 | +| **Generated at commit** | `ec22ac375` (PR #1631 merge, inferred) | -### Generator attribution +The author and generation date are recorded in the provenance added by PR #1631. That commit first registers the archive in `src/Plugins/OrientationAnalysis/test/CMakeLists.txt`. The current registration at line 154 has the same SHA512. Michael Jackson signed off on the fixture on 2026-05-28. -The original data set was designed by Michael A. Jackson with subsequent design changes and integration into the Python generation script by Claude Opus 4.7. +## How it was generated -Mike is the design owner and final authority for the dataset's correctness — he hand-reviewed the input file and signed off based on the A/B root-cause proof confirming SIMPLNX bit-identity with a surgically patched local build of the legacy source. The Claude attribution is recorded here per the V&V policy's transparency rule for LLM-generated artifacts; it does not affect the dataset's authoritativeness. +Michael Jackson designed an 11-cell, 8-feature input with selected quaternions and two material phases. The generation record in PR #1631 identifies `compute_avg_c_axis/generate_inputs.py` as the script that creates `compute_avg_c_axis/data/compute_avg_caxes_inputs.dream3d`. The tests load this input and compare generated values with literal expectations in C++; they do not load output arrays as correctness references. -## How it was generated +The input design is an 11×1×1 Image Geometry with three AttributeMatrices: -The archive contains six artifact categories, all built from a single source-of-truth Python script: +| AttributeMatrix | Tuple count | Arrays | +|---|---|---| +| `DataContainer/CellData` | 11 | `FeatureIds` (int32), `Phases` (int32), `Quats` (float32, four components) | +| `DataContainer/CellFeatureData` | 8 | Feature tuples F0 through F7; the test creates `ComputedAvgCAxes` here. | +| `DataContainer/CellEnsembleData` | 3 | `CrystalStructures`: placeholder, Hexagonal_High, Cubic_High | -``` -compute_avg_c_axis/ - README.md Inline provenance + reproducer guide + expected-value table - generate_inputs.py Builds data/compute_avg_caxes_inputs.dream3d from scratch - compare_outputs.py Three-way diff (SIMPLNX vs 6.5.171 vs patched legacy build) - data/ - compute_avg_caxes_inputs.dream3d Legacy 6.5.x-format (FileVersion 7.0) input - pipelines/ - simplnx_compute_avg_caxes.d3dpipeline SIMPLNX pipeline (ReadDREAM3D → ComputeAvgCAxes → WriteDREAM3D) - simplnx_compute_avg_caxes.json Legacy pipeline (DataContainerReader → FindAvgCAxes → DataContainerWriter) - output_legacy/ - 6_5_171_compute_avg_c_axis.dream3d Official 6.5.171 PipelineRunner output - (second .dream3d alongside it) Output of a local build of the legacy source with the surgical fixes applied - output_simplnx/ - simplnx_compute_avg_caxes.dream3d SIMPLNX nxrunner output - results/ - three_way_comparison.txt Pinned-PASS comparison report -``` +The fixture varies the Y and Z components and uses several feature populations. All rotations are about X, so the expected X component is zero. The one-dimensional layout and zero X components do not test general three-axis orientation behavior. The algorithm uses arrays and feature assignments; it does not read image spacing or origin. -### Scaffold structure +**Per-cell input design** -`ImageGeom` 11×1×1, single Z-slice, single Y-row. Three AttributeMatrices: +Quaternions use vector-scalar order `(x,y,z,w)`. -- **`CellData`** (11 tuples): per-cell `FeatureIds` (int32), `Phases` (int32), `Quats` (float32, 4 components per cell in EbsdLib `(x, y, z, w)` Vector-Scalar order). -- **`CellFeatureData`** (8 tuples — features F0 through F7; F0 is the conventional placeholder; F5 is a deliberate "declared but no cells reference it" feature to exercise the `counter==0` rescue path). -- **`CellEnsembleData`** (3 tuples — phase 0 placeholder, phase 1 `Hexagonal_High`, phase 2 `Cubic_High`). +| Cell | FeatureId | Phase | Quaternion | Purpose | +|---|---|---|---|---| +| 0 | 1 | 1 | `(0,0,0,1)` | Identity | +| 1 | 2 | 1 | `(1/2,0,0,√3/2)` | Rotation of +60° about X | +| 2 | 3 | 1 | `(0,0,0,1)` | Aligned contribution | +| 3 | 3 | 1 | `(0,0,0,1)` | Aligned contribution | +| 4 | 3 | 1 | `(0,0,0,1)` | Aligned contribution | +| 5 | 4 | 1 | `(0,0,0,1)` | First axis of an antipodal pair | +| 6 | 4 | 1 | `(1,0,0,0)` | Rotation of 180° about X | +| 7 | 6 | 2 | `(0,0,0,1)` | Non-hexagonal cell | +| 8 | 7 | 1 | `(0,0,0,1)` | First boundary-case contribution | +| 9 | 7 | 1 | `(1/2,0,0,√3/2)` | Second boundary-case contribution | +| 10 | 7 | 1 | `(-1/2,0,0,√3/2)` | Third boundary-case contribution | -### Per-cell layout +No cell references feature 0 or feature 5. `No_Hex_Phase` changes phase 1 from Hexagonal_High to Cubic_High, then requires error `-76402`. -| Cell index | FeatureId | Phase | Quaternion `(x, y, z, w)` | Notes| -|------------|-----------|-------|------------------------------------------|-------------------------------------------------------------| -| 0 | 1 | 1 | `(0, 0, 0, 1)` | Identity — c-axis aligned with sample `[001]` | -| 1 | 2 | 1 | `(0.5, 0, 0, √3/2)`| +60° about +X — c-axis at `(0, √3/2, 1/2)` | -| 2 | 3 | 1 | `(0, 0, 0, 1)` | Identity | -| 3 | 3 | 1 | `(0, 0, 0, 1)` | Identity (3 aligned cells for trivial averaging) | -| 4 | 3 | 1 | `(0, 0, 0, 1)` | Identity | -| 5 | 4 | 1 | `(0, 0, 0, 1)` | Identity — c-axis at `(0, 0, +1)` | -| 6 | 4 | 1 | `(1, 0, 0, 0)` | 180° about +X — c-axis at `(0, 0, -1)` antipodal to cell 5 | -| 7 | 6 | 2 | `(0, 0, 0, 1)` | **Cubic_High phase** — non-hex; algorithm writes NaN | -| 8 | 7 | 1 | `(0, 0, 0, 1)` | Identity — c-axis at `(0, 0, 1)` | -| 9 | 7 | 1 | `(0.5, 0, 0, √3/2)`| +60° about +X — c-axis at `(0, √3/2, 1/2)` | -| 10 | 7 | 1 | `(-0.5, 0, 0, √3/2)` | -60° about +X — c-axis at `(0, -√3/2, 1/2)` | +**Source and release evidence** -**Feature 5 has no cells** (no cell's FeatureId equals 5). This is deliberate — it exercises the `counter==0` finalize path (D1: `NaN` in SIMPLNX, `(0, 0, 1)` rescue in legacy 6.5.171). +The DREAM.3D v6.5.171 source is `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/FindAvgCAxes.cpp` at tag `v6.5.171`. It has no Cell Phases or Crystal Structures inputs. It averages all cells with positive FeatureIds, leaves feature zero at zero, and writes `(0,0,1)` for empty positive features. It does not normalize the final mean. -### Orientation convention +Current sources are `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeAvgCAxes.cpp` and `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeAvgCAxesFilter.cpp`. They supply the source evidence for D1–D5 in `../deviations/ComputeAvgCAxesFilter.md`. -Quaternions follow the EbsdLib `(x, y, z, w)` Vector-Scalar order, where for a rotation of angle θ about unit axis `n`: +Every listed release branch was inspected directly. The first three columns describe defects in the algorithm or `FromSIMPLJson()`. -``` -q = (sin(θ/2)·n_x, sin(θ/2)·n_y, sin(θ/2)·n_z, cos(θ/2)) -``` +| DREAM3D-NX releases | D3: unchecked phase lookup | D4: wrong converted array paths | D5: wrong output-name converter | +|---|---|---|---| +| v7.0.0 through v7.4.0 | Present | Present | Present | +| v7.4.1 | Present | Present | Fixed | +| v7.4.2 | Present | Fixed | Fixed | -The algorithm consumes each quaternion as a **passive** rotation matrix `R = qu2om(q)`. The crystal c-axis `[0, 0, 1]` (in the crystal reference frame) is rotated into the sample reference frame by the active operation `c_sample = R.transpose() @ [0, 0, 1]` (equivalent to using `R` as the active rotation when the convention starts from passive). The per-feature average is the arithmetic mean of all hex-cell c-axes belonging to that feature, with an antipodal-flip rule (`if c_i · running_sum < 0: c_i := -c_i`) to keep contributions in a coherent hemisphere, followed by per-feature normalization. +PR #1476 (`e45bca2a5`) fixes D5. PR #1682 (`5eb69f359`) fixes D4. PR #1568 (`4ec21e93f`) fixes D3; that fix will be in the DREAM3D-NX release after v7.4.2. DREAM.3D v6.5.171 was not affected by these three defects because it has neither the phase lookup nor the SIMPL-to-SIMPLNX conversion path. -### Per-feature closed-form expected outputs +## Canonical oracle output -Derived analytically (no DREAM3D implementation referenced). Bunge ZXZ convention with cells assigned to features per the layout above: +The canonical values are defined in `src/Plugins/OrientationAnalysis/test/ComputeAvgCAxesTest.cpp`, not in an archived filter output. -| F | Cells contributing | Hex-cell count | Expected `AvgCAxes` (SIMPLNX, post-normalize) | Magnitude | Notes | -|----|--------------------------------|----------------|------------------------------------------------------|-----------|----------------------------------------------------------------------------------| -| 0 | (placeholder) | 0 | `(NaN, NaN, NaN)` | — | counter==0 (legacy 6.5.171 writes `(0, 0, 0)` here — D1) | -| 1 | cell 0 | 1 | `(0, 0, 1)` | 1 | Single identity voxel — trivial | -| 2 | cell 1 | 1 | `(0, √3/2, 1/2)` | 1 | Single +60°-tilted voxel — trivial | -| 3 | cells 2-4| 3 | `(0, 0, 1)` | 1 | Three aligned identity voxels | -| 4 | cells 5, 6 | 2 | `(0, 0, 1)` | 1 | Antipodal pair; flip-rule resolves cell 6 to `(0, 0, +1)` before averaging | -| 5 | (none) | 0 | `(NaN, NaN, NaN)` | — | No assigned cells; counter==0 (legacy writes `(0, 0, 1)` rescue — D1) | -| 6 | cell 7 (Cubic_High, non-hex) | 0 | `(NaN, NaN, NaN)` | — | Sole F6 cell is non-hex → not summed; counter==0 (legacy writes `(0, 0, 1)` — D1) | -| 7 | cells 8-10 | 3 | `(0, √3/2, 1/2)` (post-normalize) | 1 | Precision-sensitive antipodal-flip boundary — see D2 notes below | +| DataPath | Source of expected values | +|---|---| +| `/DataContainer/CellFeatureData/ComputedAvgCAxes` | Class 1 component values for F1–F4 and NaN expectations for F0, F5, and F6; Class 4 magnitude expectations for F7 and all hex-valid features. | -### Feature 7 — the diagnostic feature +**Analytical construction** -Feature 7's cells have c-axes at `(0, 0, 1)`, `(0, √3/2, 1/2)`, and `(0, -√3/2, 1/2)`. The unweighted arithmetic mean is: +For a quaternion representing angle θ about unit axis `n`, use `q = (sin(θ/2)n, cos(θ/2))`. The passive orientation matrix `R(q)` gives the sample C-axis as `R(q).T × (0,0,1)`. Process each feature's cells in cell-index order. Flip a contribution when its dot product with the running direction is negative. Normalize the final sum for each feature with at least one hexagonal contribution. -``` -(0, 0, 1) -+ (0, √3/2, 1/2) -+ (0, -√3/2, 1/2) -= (0, 0, 2) -/ 3 = (0, 0, 2/3) -``` +| Feature | Contributions | Expected result | Check | +|---|---|---|---| +| F0 | None | `(NaN,NaN,NaN)` | Three `isnan` assertions | +| F1 | Identity | `(0,0,1)` | Three component comparisons | +| F2 | +60° about X | `(0,√3/2,1/2)` | Three component comparisons | +| F3 | Three identity cells | `(0,0,1)` | Three component comparisons | +| F4 | Identity and 180° about X | `(0,0,1)` after the antipodal flip | Three component comparisons | +| F5 | None | `(NaN,NaN,NaN)` | Three `isnan` assertions | +| F6 | One Cubic_High cell, excluded | `(NaN,NaN,NaN)` | Three `isnan` assertions | +| F7 | Identity, +60° X, -60° X | Unit magnitude | Three non-NaN checks and one magnitude comparison | -magnitude `2/3 ≈ 0.6667` — the **unnormalized** value legacy 6.5.171 wrote. After SIMPLNX's per-feature normalize (added during the V&V cycle's Phase 7 refactor), the SIMPLNX output is `(0, 0, 1)` magnitude 1 — but only when the running-average antipodal-flip rule fires cleanly. +Component and magnitude comparisons use an absolute margin of `1e-5`. This is a dimensionless vector tolerance, not an angular tolerance. A separate dynamic section checks unit magnitude for F1, F2, F3, F4, and F7. The five ordinary test cases use the supplied in-core CI result. All pass in CI at `419f9db35`. -The antipodal-flip rule (`if c_i · running_sum < 0: c_i := -c_i`) is precision-sensitive at the cancellation boundary: when `running_sum` for cells 8-9 is `(0, √3/2, 3/2)`, evaluating `c_10 · running_sum = (0, -√3/2, 1/2) · (0, √3/2, 3/2) = -3/4 + 3/4 = 0` to mathematical precision but slightly positive or negative in IEEE 754 depending on accumulation order. SIMPLNX and the patched local build of the legacy source happen to compute it as positive (flip does NOT fire), producing `(0, √3/2, 1/2)` post-normalize; the unnormalized sum direction matches but legacy 6.5.171's unnormalized magnitude is `2/3`, not 1 — D2 surfaces here. +**F7 sign-boundary derivation** -This was deliberately placed on the boundary to validate the Class 4 invariant `||AvgCAxes|| == 1.0` post-normalize (SIMPLNX side) and to surface D2's precision-sensitive boundary as a documented deviation rather than an unexplained difference. +The first two ideal axes sum to `(0,√3/2,3/2)`. The third axis `(0,-√3/2,1/2)` has dot product `-3/4 + 3/4 = 0` with that sum. Rounding can determine whether the strict negative-cosine branch fires. -## Canonical oracle output +| Sign decision for the third axis | Mean before normalization | Direction after normalization | +|---|---|---| +| Keep its sign | `(0,0,2/3)` | `(0,0,1)` | +| Flip its sign | `(0,1/√3,1/3)` | `(0,√3/2,1/2)` | -| DataPath | Source of expected values | -|---------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------| -| `/DataContainer/CellFeatureData/ComputedAvgCAxes` (SIMPLNX) | Class 1 analytical (closed-form set-arithmetic on the quaternion-rotated c-axes per the per-cell layout above) + Class 4 invariant `||AvgCAxes[i]|| == 1.0` for hex-valid features, `NaN` whenever `counter[i] == 0`. | -| `/DataContainers/DataContainer/CellFeatureData/AvgCAxes` (legacy) | Same Class 1 analytical (the surgically patched local build of the legacy source is bit-identical to SIMPLNX); legacy 6.5.171 differs at F0/F5/F6 (D1) and F7 (D2). | +Both pre-normalization means have magnitude `2/3`. Both normalized directions have magnitude 1, but their angle is 60°. The test asserts magnitude for F7 and does not assert either direction. This derivation explains D2 without using a legacy output as an oracle. -The expected values are checked in the unit test (`test/ComputeAvgCAxesTest.cpp::"Class 1 Oracle (hand-built dataset)"`) via per-feature `Approx(expected).margin(1e-5f)` checks for each component (or `std::isnan` for the D1 features). Tolerance `1e-5°` is comfortably below any expected float32 precision noise from the Eigen double-precision intermediate. +**Separate boundary fixture** + +`genuine HDF5 4096-block tail oracle` constructs its input in C++ and does not use this archive. It has 4,097 cells, including 4,095 background cells. Feature 1 receives an identity contribution at index 4,095 and a +60° X contribution at index 4,096. Their normalized mean is `(0,1/2,√3/2)`. The hidden test checks this vector, feature-zero NaNs, tuple and component shapes, and HDF5 input/output store formats. ## Oracle provenance (Classes 2, 3, 5 only) -Class 1 (Analytical) primary, Class 4 (Invariant) companion. No reference-library invocation, no published-paper figure reproduction, no expert-visual sign-off. +Not applicable. The correctness expectations use Classes 1 and 4. DREAM.3D output and output from a local proof version do not define the expected values. ## Second-engineer oracle review -- **Reviewer:** *Skipped — recorded reason:* Class 1 derivation is set-arithmetic on quaternion-rotated c-axes for an 11-cell, 8-feature hand-built fixture. -- **Date:** JUNE 5, 2026 +- **Reviewer:** skipped +- **Date:** 2026-06-05 +- **Skip reason:** The existing provenance records an analytical derivation from quaternion-rotated C-axes for the 11-cell, 8-feature fixture. ## Regenerated to fix a circular-oracle situation? -**Yes.** This archive replaces the retired `7_2_AvgCAxis.tar.gz` (SHA512 not retained in this repo). The retired archive contained exemplar arrays whose reference values were produced by a "special build of DREAM3D 6.6.379 with micro-texture bug fixes" — i.e., reference values produced by a non-baseline DREAM3D build, not by an independent oracle. Per V&V policy line 33 ("'Legacy 6.5.171 produced this output' is never a valid oracle for correctness"), the retired archive failed the independence test even more sharply: it was a *modified* legacy build, not even the baseline. The hand-built Class 1 oracle in this archive is independent of any DREAM3D implementation, and is cross-validated by the A/B root-cause proof confirming SIMPLNX matches a surgically patched local build of the legacy source bit-for-bit. - -The `download_test_data` line for the retired archive was removed from `test/CMakeLists.txt` as part of the V&V cycle's retirement work; the new archive's `download_test_data` line + SHA512 are wired in at the same location. +Yes. PR #1631 replaces `7_2_AvgCAxis.tar.gz` with `compute_avg_c_axis.tar.gz`. Its generation record identifies a local proof version as the producer of the retired reference arrays. The replacement tests consume hand-built inputs and use analytical C++ expectations instead of those reference arrays. diff --git a/src/Plugins/OrientationAnalysis/vv/provenance/ComputeAvgOrientationsFilter.md b/src/Plugins/OrientationAnalysis/vv/provenance/ComputeAvgOrientationsFilter.md index 7c5128a5a1..a7244c0788 100644 --- a/src/Plugins/OrientationAnalysis/vv/provenance/ComputeAvgOrientationsFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/provenance/ComputeAvgOrientationsFilter.md @@ -1,76 +1,97 @@ -# Exemplar / Oracle Provenance: ComputeAvgOrientationsFilter +# Exemplar Archive Provenance: 7_ComputeAvgOrientation_v2.tar.gz -This filter's V&V oracle is **inline hand-built data** encoded directly in `test/ComputeAvgOrientationsTest.cpp` — there is no exemplar `.tar.gz` archive for the correctness oracle. This sidecar documents the oracle derivation and the retirement of the prior circular-oracle archive. +The archive is retired. This sidecar records its origin, the inline replacement oracles, release-source evidence and the dated legacy comparison. -## Retired archive (circular oracle) +## Archive identity | Field | Value | |---|---| -| **Archive** | `7_ComputeAvgOrientation_v2.tar.gz` | +| **Archive** | `7_ComputeAvgOrientation_v2.tar.gz` (retired by PR #1645) | | **SHA512** | `2c2a691f1da301c449c20bafec65512d5134db38384ac7cb4c910880ccd87a260a5f011e905f35b97abff3952309f109c737c63ec3c833708926827a62a92efc` | -| **Status** | **Retired 2026-06-30** (replaced by inline oracle) | +| **Used by tests** | None now. The removed `OrientationAnalysis::ComputeAvgOrientations` case compared archived outputs. | +| **Generated by** | Michael Jackson (inferred from PR #1577) | +| **Generated on** | Not recorded | +| **Generated at commit** | `41c079dd2` (PR #1577 merge, inferred) | -This archive bundled the filter's own computed `AvgQuats` / `Watson Avg Quats` / `vMF Avg Quats` / Euler / Kappa arrays as the comparison target. It was regenerated from post-fix SIMPLNX output in PR #1577 and therefore could not serve as an independent oracle (any bug present at capture time would be confirmed forever). It is named in the V&V audit's cross-cutting circular-oracle list. The V&V replaces it with the Class 1/2/4 oracle below. +The hash above matches `src/Plugins/OrientationAnalysis/test/CMakeLists.txt` at `41c079dd2`, the first commit to register this archive name. PR #1645 (`4f9bbc378`) removes the registration. The current CMake file contains no ComputeAvgOrientations archive entry. -## Class 1 (Analytical) — Rodrigues average, Triclinic symmetry +## How it was generated -Triclinic (`CrystalStructures = 4`) has only the identity proper-rotation operator, so `getFZQuat` is a no-op and `getNearestQuat(ref, q)` reduces to choosing the double-cover representative (`q` or `−q`) nearest `ref`. With all input quaternions chosen in the northern hemisphere (`w > 0`), the Rodrigues running average reduces to the closed form **`normalize(Σ qᵢ)`, positive-oriented** — a hand-computable quaternion mean. +PR #1577 registered the archive with expected outputs computed by SIMPLNX, including Rodrigues, vMF and Watson arrays. PR #1645 replaced that comparison with inputs and expected values defined in `src/Plugins/OrientationAnalysis/test/ComputeAvgOrientationsTest.cpp`. The current tests construct arrays in C++; they do not load this archive. The Class 1 fixtures use hand-selected rotations, while the Class 2 fixture copies 22 EbsdLib reference quaternions. -Quaternions below are in `(x, y, z, w)` storage order (the order the filter reads/writes). `Rz(θ)` denotes a rotation by θ about z = `(0, 0, sin(θ/2), cos(θ/2))`. +**Legacy comparison evidence.** The run against stock DREAM.3D v6.5.171 on 2026-09-17 used SIMPLNX commit `8b80721c20853d891d5040f9134315af1d93b2b6`. The runner identity, four shared-input cases and 12 successful executions are recorded in `vv_work:ComputeAvgOrientationsFilter/ReadMe.md`. Input construction and pipeline definitions are in `vv_work:ComputeAvgOrientationsFilter/scripts/make_inputs.py` and `vv_work:ComputeAvgOrientationsFilter/scripts/make_pipelines.py`. Results are in `vv_work:ComputeAvgOrientationsFilter/comparison/comparison_summary.md` and its per-feature CSV files. -| Fixture | Voxels (phase) | Expected AvgQuats `(x,y,z,w)` | Derivation | -|---|---|---|---| -| **F0** background | (phase 0, ignored) | `(0,0,0,1)` identity | `count==0` finalize → identity | -| **F1** single identity | 1× identity | `(0, 0, 0, 1)` | single voxel → itself | -| **F2** single Rz(90°) | 1× `(0,0,0.70710678,0.70710678)` | `(0, 0, 0.70710678, 0.70710678)` | single voxel → itself (Triclinic FZ no-op) | -| **F3** mean of identity & Rz(90°) | identity + Rz(90°) | `(0, 0, 0.38268343, 0.92387953)` = **Rz(45°)** | `normalize(identity + Rz90) = Rz(45°)` (see derivation note) | -| **F4** 3× Rz(90°) | 3× Rz(90°) | `(0, 0, 0.70710678, 0.70710678)` = Rz(90°) | N identical → same orientation | -| **F5** empty feature | none | `(0,0,0,1)` identity | `count==0` finalize → identity | +| Case | Real populated features matching stock DREAM.3D v6.5.171 at `1e-6` | Other tuples | +|---|---|---| +| `case_A_small_in100_subset` | 408 of 408 | Empty feature zero differs (D3). | +| `case_B_forcing_d2_d3` | 2 of 2 | Populated feature zero differs (D2); two empty nonzero tuples differ (D3). | +| `case_C1_unit_test_analytical` | 4 of 4 | Empty feature zero and empty feature 5 differ (D3). | +| `case_C2_unit_test_cubic_invariant` | 1 of 1 | Empty feature zero differs (D3). | -**F3 derivation:** `identity + Rz(90°) = (0,0,sin45°, 1+cos45°)`; for a general `Rz(α)`, `normalize(identity + Rz(α)) = (0,0,sin(α/4),cos(α/4)) = Rz(α/2)` because `1+cos(α/2)=2cos²(α/4)` and `sin(α/2)=2sin(α/4)cos(α/4)`. For α=90° → `Rz(45°) = (0,0,sin22.5°,cos22.5°) = (0,0,0.38268343,0.92387953)`. +All 415 real populated features match within `1e-6`; zero of four cases match over every output tuple. Small IN100 has maximum stock-versus-SIMPLNX differences of `8.94e-8` in AvgQuats and `4.77e-7` radians in AvgEulerAngles. No real-feature quaternion sign flips occur. A local proof version corrects D3 and matches all tuples in three of four cases; case B retains the intentional D2 difference. These comparisons explain migration behavior at the recorded SIMPLNX commit. -**Euler note:** A pure z-rotation has `Φ = 0`, a gimbal-lock case where Bunge `φ1` and `φ2` are individually non-unique (only `φ1+φ2` is determined). Therefore Euler is asserted tightly only for the identity fixture (`(0,0,0)`); for z-rotation fixtures the quaternion is the tight assertion and Euler is checked as an invariant (`Φ ≈ 0`, all finite). +The algorithm source changed after the run: `4ec21e93f` (2026-09-20, PR #1568) adds dispatch, bulk transfers and grouped directional processing; `a33c370ca` (2026-09-25, PR #1697) changes progress messaging. The report therefore does not use the 2026-09-17 comparison as a current-algorithm result. -### Class 4 (Invariant) companions — Rodrigues -- Output quats unit-norm and northern-hemisphere (`w ≥ 0`). -- Zero-voxel feature → identity `(0,0,0,1)` + zero Euler. -- z-rotation fixtures (F2–F4): Euler asserted as the invariant `Φ ≈ 0`, all components finite (encoded in `Rodrigues Analytical Oracle`). -- Voxel-ordering independence: F3 with voxels in order [identity, Rz90] vs [Rz90, identity] → identical result (both accumulate to `(0,0,0.7071,1.7071)` pre-normalize). Encoded as `Rodrigues Voxel Ordering Independence` (DYNAMIC_SECTION over both orderings). +**Release-source evidence.** Each of the ten `upstream/release/v` branches was inspected at `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeAvgOrientations.cpp`. The legacy comparison uses `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/FindAvgOrientations.cpp` at the DREAM.3D `v6.5.171` tag. -## Class 4 (Invariant) — Rodrigues under cubic symmetry (issue #1660) +| Releases | Source result | +|---|---| +| DREAM3D-NX v7.0.0 through v7.4.1 | Rodrigues omits feature-zero finalization, divides empty features by zero and performs unchecked phase and crystal-structure lookups (D3, D6, D7). These releases have no vMF/Watson worker. | +| DREAM3D-NX v7.4.2 | Contains the D3 fix from PR #1577, the D6/D8/D9 fixes from PR #1645, and the D5/D7 fixes from PR #1664. No listed defect remains in this release. | + +D5, D8 and D9 occur only between the introduction of directional sampling in PR #1577 and their fixes. No release from v7.0.0 through v7.4.2 contains those defects. DREAM.3D v6.5.171 was affected by D3, D6 and D7; it was not affected by D5, D8 or D9 because it has no directional-sampling worker. + +## Canonical oracle output + +The paths below identify C++ fixture arrays, not archived filter outputs. Quaternion components use `(x,y,z,w)` order. + +| DataPath | Source of expected values | +|---|---| +| `/ImageGeom/Cell Feature Data/AvgQuats` | Class 1 hand-derived means; Class 4 unit norm, nonnegative scalar component, cubic symmetry and two-ordering checks. | +| `/ImageGeom/Cell Feature Data/AvgEulerAngles` | Identity and empty features give zero; pure z rotations have finite components and middle Euler angle zero. | +| `/ImageGeom/Cell Feature Data/vMF Avg Quats`, `/ImageGeom/Cell Feature Data/vMF Kappas` | Class 2 fixed EbsdLib VMF estimates; Class 4 single-sample and empty-feature rules. | +| `/ImageGeom/Cell Feature Data/Watson Avg Quats`, `/ImageGeom/Cell Feature Data/Watson Kappas` | Class 2 fixed EbsdLib Watson estimates; Class 4 single-sample and empty-feature rules. | +| `/ImageGeom/Cell Feature Data/vMF Avg EulerAngles`, `/ImageGeom/Cell Feature Data/Watson Avg EulerAngles` | Class 4 empty-feature NaN assertions; multi-sample cases also compare Direct and Scanline values. | + +For the triclinic fixtures, the selected quaternion representatives have positive scalar components. Their normalized sum gives the Rodrigues expectation. With `Rz(theta) = (0,0,sin(theta/2),cos(theta/2))`, the six feature fixtures are: -The Triclinic fixtures above make `getNearestQuat` a sign-pick no-op, so they cannot exercise the symmetry-reduction interaction that is the Rodrigues path's value-add. The `Rodrigues Cubic Symmetry Invariant` test closes that gap with an implementation-independent expectation: +| Fixture | Input | Expected AvgQuats | +|---|---|---| +| F0 | One phase-zero cell | `(0,0,0,1)` | +| F1 | Identity | `(0,0,0,1)` | +| F2 | Rz90 | `(0,0,0.70710678,0.70710678)` | +| F3 | Identity and Rz90 | `(0,0,0.38268343,0.92387953)` | +| F4 | Three Rz90 samples | `(0,0,0.70710678,0.70710678)` | +| F5 | No cells | `(0,0,0,1)` | -One `Cubic_High` feature is fed the **same physical orientation** `Rz(30°)` as five different representations — the cubic-equivalent z-rotations `Rz(30°+90°k)` for k = 1..3 (Rz(90°) is a cubic symmetry operator), the base `Rz(30°)`, and the negated double-cover representative `−Rz(30°)` — in scrambled order so the first-voxel reset operates on `Rz(120°)`. Because rotations about a common axis commute, every representation has an exact distance-0 symmetry-equivalent `Rz(30°)`, so `getNearestQuat` must pick exactly `Rz(30°)` for each voxel and the average must finalize to `Rz(30°) = (0, 0, sin15°, cos15°) = (0, 0, 0.25881905, 0.96592583)` (asserted at ±1e-5). This exercises the 24-operator cubic branch, the reset-to-identity first-voxel branch, the double-cover canonicalization, and the count weighting — independent of the legacy A/B comparison. +For F3, `normalize(identity + Rz(alpha)) = Rz(alpha/2)`, using the sine and cosine half-angle identities. The quaternion tolerance is `1e-6`. Pure z rotations have nonunique outer Euler angles, so the test checks finite values and a zero middle angle instead of selecting one arbitrary outer-angle pair. The cubic invariant uses five symmetry/sign representations of Rz30 and expects `(0,0,0.25881905,0.96592583)` within `1e-5`. -## Class 2 (Reference — EbsdLib, trusted) — vMF / Watson average +Directional single-sample features return the fundamental-zone quaternion with kappa zero. Empty features keep NaNs. Phase-zero and out-of-range-phase cells are excluded. The directional methods use the phase of the highest-index cell with a positive, in-range phase; if that phase has an unsupported crystal structure, the feature remains NaN. Rodrigues selects symmetry separately for each contributing cell. The phase-order fixture checks this distinction. -The vMF/Watson EM math is owned and tested by EbsdLib (`EbsdLib/Source/Test/DirectionalStatsTest.cpp`). Its `DirectionalStatsTest:VMF` / `:Watson` cases run **at this filter's exact configuration** (`numEM=5, numIter=10, seed=43514`, Cubic_High ops) over a fixed 22-quaternion set (`detail::k_TestQuats`, each FZ-reduced via `cubicOps->getFZQuat`), and hard-assert the EM result. The filter performs the identical pipeline (`getFZQuat` → `EMforDS(43514,…)` → `positiveOrientation`). +All pass in CI at `419f9db35`. -**Oracle fixture:** one SIMPLNX feature whose 22 voxels are exactly `detail::k_TestQuats` (Phase 1, `CrystalStructures[1] = Cubic_High = 1`). The filter's per-feature result must reproduce EbsdLib's documented values (which have `w > 0`, so `positiveOrientation` is a no-op): +## Oracle provenance (Classes 2, 3, 5 only) -| Method | Expected AvgQuats `(x,y,z,w)` | Expected Kappa | Source | -|---|---|---|---| -| **vMF** | `(0.3322000547718371, −0.1964639452260062, 0.2450656693404858, 0.8893749825279105)` | `88.9943042750539774` | `DirectionalStatsTest.cpp:202–206` | -| **Watson** | `(0.2948298270586034, −0.2106011604618418, 0.2378717152588106, 0.9011878668560466)` | `30.5730272919979669` | `DirectionalStatsTest.cpp:249–253` | +### Class 2 — Reference implementation -This proves the filter feeds EbsdLib correctly and lands the result in the right output tuple, **without re-deriving any EM math** (per the "test the value-add, not upstream" rule). +- **Library:** EbsdLib `DirectionalStats`, Class 2 reference. +- **Exact version:** The reference constants match `Source/Test/DirectionalStatsTest.cpp` at EbsdLib tag `v3.0.0`. +- **Runtime:** C++ `DirectionalStatsTest:VMF` and `DirectionalStatsTest:Watson`; Cubic_High symmetry, 5 EM restarts and 10 iterations. +- **Random seed:** `43514`. +- **Script in archive:** None. `vMF/Watson EbsdLib Reference Oracle` in `src/Plugins/OrientationAnalysis/test/ComputeAvgOrientationsTest.cpp` contains the 22 input quaternions and expected constants. -**Tolerances (as shipped in the test):** quaternion components ±5e-3 absolute (`k_QuatMargin`), kappa ±2% relative (`k_KappaRel`). EbsdLib's own test asserts at ±1e-6, but it feeds `EMforDS` double-precision quaternions; the filter round-trips the same inputs through a `float32` DataArray, and the iterative EM estimate — kappa especially, being a concentration (inverse-spread) parameter — amplifies those input perturbations well beyond the raw float32 epsilon. The margins are set an order of magnitude above the observed cross-platform deltas for robustness while remaining far below any physically meaningful divergence (a 5e-3 quaternion delta ≈ 0.6° misorientation; the fixture's kappa values are ~89 and ~31). +| Method | Expected quaternion `(x,y,z,w)` | Expected kappa | +|---|---|---| +| vMF | `(0.3322000547718371,-0.1964639452260062,0.2450656693404858,0.8893749825279105)` | `88.9943042750539774` | +| Watson | `(0.2948298270586034,-0.2106011604618418,0.2378717152588106,0.9011878668560466)` | `30.5730272919979669` | -### Class 4 (Invariant) companions — vMF / Watson -- Single-voxel feature → `muhat` = that voxel's FZ quat, `kappa == 0` (filter shortcut, EM skipped). -- Zero-voxel feature → all vMF/Watson outputs `NaN` (NaN pre-fill, never overwritten). -- Output quats unit-norm + northern-hemisphere; results at the feature-index tuple (not voxel index). -- Phase gating (regression, issue #1659): a phase-0 voxel inside a feature must be excluded from the gather — the counting pass, the gather loop, and the Rodrigues path all gate identically on `phase > 0`. Encoded as `vMF/Watson Ignores Phase-0 Voxels` (single-voxel shortcut preserved + EM path lands on the valid orientation, cross-checked against Rodrigues). -- Guards (issue #1661): out-of-range `Phases` values and unknown/unsupported crystal-structure values are excluded from both paths and reported as warnings (-54672 / -54671, never silent); fully-excluded features finalize to identity (Rodrigues) / NaN (vMF/Watson). Encoded as `Unknown Crystal Structure and Out-Of-Range Phase Guards`. -- Multi-phase features: the vMF/Watson `featureIdToPhaseMap` is last-writer-wins — the feature's crystal structure comes from the phase of its highest-index voxel (Rodrigues is per-voxel). Documented in the algorithm and the filter docs; features are normally single-phase. +The filter test uses float32 inputs, component margin `5e-3` and relative kappa tolerance 2%. It checks sample gathering, fundamental-zone reduction and output placement against these fixed constants. It does not derive the EbsdLib estimator independently. ## Second-engineer oracle review -- **Reviewer:** Michael Jackson (technical authority) -- **Date:** 2026-07-16 +- **Reviewer:** Michael Jackson (technical authority). +- **Date:** 2026-07-16. -## Reproduction +## Regenerated to fix a circular-oracle situation? -The toy data is constructed entirely in `test/ComputeAvgOrientationsTest.cpp` from the literals above (no external files). To reproduce: read the fixture tables here, build the arrays, run the filter with the relevant method toggles, and compare against the expected columns. +The retired archive used outputs generated by the filter in PR #1577 as its expected values. PR #1645 removed that circular comparison and the download registration. The replacement oracle uses analytical values, fixed EbsdLib reference values and invariants in C++; no replacement archive is required. diff --git a/src/Plugins/OrientationAnalysis/vv/provenance/ComputeFaceIPFColoringFilter-circular-oracle.md b/src/Plugins/OrientationAnalysis/vv/provenance/ComputeFaceIPFColoringFilter-circular-oracle.md index ffc04488a2..455514538d 100644 --- a/src/Plugins/OrientationAnalysis/vv/provenance/ComputeFaceIPFColoringFilter-circular-oracle.md +++ b/src/Plugins/OrientationAnalysis/vv/provenance/ComputeFaceIPFColoringFilter-circular-oracle.md @@ -1,27 +1,50 @@ -# Exemplar Provenance: ComputeFaceIPFColoringFilter (no archive — circular-oracle retirement) +# Exemplar Archive Provenance: 6_6_Small_IN100_GBCD.tar.gz -This filter's V&V uses a **Class 1 (Analytical)** oracle encoded inline in the test source, so it owns **no exemplar archive**. This sidecar exists only to record the retirement of the prior circular-oracle comparison, per the V&V policy's circular-oracle requirement. +This sidecar records the shared input archive and the replacement of its face-color comparison with an analytical oracle. ---- +## Archive identity + +| Field | Value | +|---|---| +| **Archive** | `6_6_Small_IN100_GBCD.tar.gz` | +| **SHA512** | `543e3bdcee24ff9e5cd80dfdedc39ceef1529e7172cebd01d8e5518292ffdf4e0eb2e79d75854cb3eaca5c60e19c861ca67f369e21b81c306edb66327f47a1e3` | +| **Used by tests** | `OrientationAnalysis::ComputeFaceIPFColoringFilter: Invalid filter execution`; `OrientationAnalysis::ComputeFaceIPFColoringFilter: ColorKey choice reaches algorithm` | +| **Generated by** | Michael Jackson (inferred from PR #482) | +| **Generated on** | — | +| **Generated at commit** | `10303199d` (PR #482 merge, inferred) | + +The first registration is `d8d04a482`, authored by Michael Jackson on 2023-03-09 and imported through PR #482 on 2023-03-17. That commit, the PR merge and current `src/Plugins/OrientationAnalysis/test/CMakeLists.txt` register the same archive name and SHA512. These are source-history dates, not the archive generation date. + +## How it was generated + +The archive supplies `6_6_Small_IN100_GBCD/6_6_Small_IN100_GBCD.dream3d`, a Small IN100 dataset with surface-mesh and feature arrays. The current tests load those arrays for invalid-selection checks and three color-key executions. Their independent analytical inputs are constructed in `src/Plugins/OrientationAnalysis/test/ComputeFaceIPFColoringTest.cpp`, rather than read from the archive. + +The separate legacy comparison fixture is synthetic. `vv_work:ComputeFaceIPFColoringFilter/build_fixture.py` creates four faces with cubic and hexagonal features, identity Euler angles and corner-aligned normals. `vv_work:ComputeFaceIPFColoringFilter/legacy_comparison_summary.md` records its 2026-06-30 results; the deviations sidecar preserves those results and identifies the later algorithm changes. ## Canonical oracle output +The canonical values live in the test code. No archived RGB array supplies expected values to this filter's current tests. + | DataPath | Source of expected values | |---|---| -| `/Data/NXFaceIPFColors 0` (first color) | Class 1 analytical — IPF standard-triangle corner primaries hand-derived in `test/ComputeFaceIPFColoringTest.cpp` ("Class 1 Oracle - mixed-phase analytical"). cubic `<100>`→(255,0,0), cubic `<111>`→(0,0,255). | -| `/Data/NXFaceIPFColors 1` (second color) | Class 1 analytical — hex c-axis→(255,0,0), hex basal→(0,255,0) (red channel 0); black where the adjacent feature label ≤ 0. | +| `/Data/NXFaceIPFColors 0` | Class 1, five literal RGB tuples: red, black, red, blue, red. | +| `/Data/NXFaceIPFColors 1` | Class 1, five literal RGB tuples: green, green, black, blue, red. Class 4 also requires face 0's red channel to equal zero. | + +The five-face fixture uses feature orientations `(pi/2,0,0)`, `(pi/3,0,0)` and `(0,0,0)`. Cubic symmetry preserves the red `<100>` and blue `<111>` corners under the first rotation. The hexagonal rotation preserves its basal green and c-axis red corners. Exact basal green follows the current TSL convention; the zero-red invariant is independent of the green/blue assignment. All pass in CI at `419f9db35`. + +## Oracle provenance (Classes 2, 3, 5 only) -No Class 2/3/5 provenance block applies (Class 1 oracle lives in the test code). +Not applicable. The correctness oracles are Classes 1 and 4 and are encoded in the test source. The legacy comparison explains migration differences. ## Second-engineer oracle review -- **Reviewer:** Michael Jackson (technical authority) -- **Date:** 2026-07-16 +- **Reviewer:** Michael Jackson (technical authority). +- **Date:** 2026-07-16. -## Regenerated to fix a circular-oracle situation? +The report's sign-off names no second engineer. -**YES — comparison retired, not regenerated.** Before this V&V cycle the test `OrientationAnalysis::ComputeFaceIPFColoringFilter: Valid filter execution` compared the filter's output against a `SurfaceMeshFaceIPFColors` array baked into the shared archive `6_6_Small_IN100_GBCD.tar.gz`. That baked array was generated by legacy DREAM3D 6.6-era tooling, which carried the same #1635 bug that was later ported verbatim into SIMPLNX — so it is byte-identical to the filter's own pre-fix output and functioned as a **circular oracle with respect to the defect** (the deviations file's description of it as "authentic 6.5.171-class output" and this file's "own pre-fix output" describe the same array; the two characterizations are equivalent because the bug was ported unchanged). It encoded the bug (notably black Phase-2 colors on `feature1`-invalid boundary faces of the single-phase Small IN100 mesh), so applying the `phase1`→`phase2` fix correctly broke the test. +## Regenerated to fix a circular-oracle situation? -Rather than regenerate the baked array from post-fix output (which would re-establish a circular oracle, and which is impractical because `6_6_Small_IN100_GBCD.tar.gz` is shared by other GBCD tests), the comparison was **retired** and replaced by the inlined Class 1 analytical oracle. Real-data exercise of the algorithm is retained by `ComputeFaceIPFColoringFilter: ColorKey choice reaches algorithm`, which runs the filter on the full Small IN100 GBCD surface mesh without depending on any baked exemplar. +The archive was not regenerated. PR #1646 replaces `Valid filter execution`, which compared both sides with the archived `SurfaceMeshFaceIPFColors`, with `Class 1 Oracle - mixed-phase analytical`. The archived array carries the same wrong-phase defect as the old algorithm, so agreement with it could not detect D1. The 2026-06-30 comparison summary records the 120,000 exterior-face differences after correction. -The shared archive `6_6_Small_IN100_GBCD.tar.gz` itself is **not** modified or retired — only this filter's use of its `SurfaceMeshFaceIPFColors` array as a correctness oracle. +The shared archive remains an input to the invalid-selection and color-key tests. Its `SurfaceMeshFaceIPFColors` array is no longer this filter's oracle. The change and the replacement fixture are visible in commit `f5b383be6` (PR #1646). diff --git a/src/Plugins/OrientationAnalysis/vv/provenance/ComputeFeatureFaceMisorientationFilter.md b/src/Plugins/OrientationAnalysis/vv/provenance/ComputeFeatureFaceMisorientationFilter.md new file mode 100644 index 0000000000..5f8824f500 --- /dev/null +++ b/src/Plugins/OrientationAnalysis/vv/provenance/ComputeFeatureFaceMisorientationFilter.md @@ -0,0 +1,76 @@ +# Exemplar Archive Provenance: Inlined in Test + +This sidecar records the inline analytical fixtures and the archived legacy comparison for `ComputeFeatureFaceMisorientationFilter`. + +## Archive identity + +| Field | Value | +|---|---| +| **Archive** | None; the tests construct their inputs in C++. | +| **SHA512** | Not applicable; no current test consumes an archive registered in `src/Plugins/OrientationAnalysis/test/CMakeLists.txt`. | +| **Used by tests** | `Curated Data`, `Phase Index Bounds`, and the hidden `genuine HDF5 65536-block tail oracle` in `src/Plugins/OrientationAnalysis/test/ComputeFeatureFaceMisorientationTest.cpp`. | +| **Generated by** | Fixture source: Nathan Young (PR #1618); Michael Jackson (PR #1631). | +| **Generated on** | Not applicable; each test constructs its data when it runs. | +| **Generated at commit** | `7cff7f2ea` (PR #1618, initial curated fixture); `ec22ac375` (PR #1631, Trigonal_High extension and corrected cubic expectation). | + +## How it was generated + +`curated::CreateTestDataStructure()` constructs a synthetic mesh with 111 vertices, 37 faces, 45 feature tuples and 13 ensemble tuples. Each Laue class has four feature orientations, with Euler angles `(0,0,0)`, `(45,0,0)`, `(90,0,0)` and `(180,0,0)` in degrees. The test runs `ChangeAngleRepresentationFilter` to convert degrees to radians and `ConvertOrientationsFilter` to create quaternions. Expected output angles are literal analytical values in the test; they are not computed from filter output. + +Feature 0 is a sentinel. Features 1–44 form 11 groups of four, with phases 1–11 and Laue indices 0–10. Ensemble tuple 0 contains 999; tuple 12 repeats Laue index 10 and is unused by the curated faces. Faces 0–29 contain the first ten classes, faces 30–33 contain two background and two mixed-phase cases, and faces 34–36 contain Trigonal_High. Each triangle has three separate vertices; the angle algorithm reads face labels and feature data, so vertex positions do not determine the expected angles. + +`Phase Index Bounds` constructs one face with labels 1 and 2, three feature tuples and two ensemble tuples. Its sections use phases `(2,2)` and `(2,1)` to check the error and skip paths. The hidden boundary fixture constructs 65,537 faces and places analytical 15° and 30° values at tuples 65,535 and 65,536. PR #1568 (`4ec21e93f`) adds the bounds fixture; PR #1750 (`8914ca68f`) adds the boundary fixture. + +Run against DREAM.3D v6.5.171 on 2026-09-17: 639,631 of 640,880 comparable faces match within 0.0001°. The run predates algorithm commit `4ec21e93f` (2026-09-20, PR #1568), which adds phase validation and changes the placement of the Crystal Structures lookup. These archived results describe the algorithm used on the run date. + +`vv_work:ComputeFeatureFaceMisorientationFilter/ReadMe.md` identifies the stock application and its `SIMPLib Version 1.2.828.f45085c83` banner. `vv_work:ComputeFeatureFaceMisorientationFilter/comparison/summary.md` and `vv_work:ComputeFeatureFaceMisorientationFilter/comparison/difference_report.md` record the results below. The comparison takes the float64 Euclidean norm of the legacy axis-times-angle vector and compares it with the SIMPLNX scalar angle on valid same-phase Hexagonal_High and Cubic_High faces. + +| Input | Comparable faces | DREAM.3D v6.5.171 matches within 0.0001° | Maximum difference | +|---|---:|---:|---:| +| `fixture_37faces` | 6 | 6 | 0.0000076294° | +| `random_hex_cubic` | 4,400 | 4,389 | 0.000267338° | +| `smallin100_mesh` | 636,474 | 635,236 | 0.001925168° | + +A local proof version matches all 640,880 comparable faces within 0.0001°. All 120,154 invalid faces give SIMPLNX NaN and legacy zero vectors; all 127 faces in classes unsupported by the legacy filter give finite SIMPLNX values and legacy zero vectors. These checks explain migration differences; the independent correctness oracle remains the analytical test source. + +`vv_work:ComputeFeatureFaceMisorientationFilter/scripts/make_inputs.py` defines the comparison inputs. `vv_work:ComputeFeatureFaceMisorientationFilter/scripts/compare.py` defines the magnitude mapping and tolerances. `vv_work:ComputeFeatureFaceMisorientationFilter/comparison/audit.json` records input-array checks and comparison counts. The random input uses a saved payload because the original generator did not reproduce it; its claimed 400 zero-angle pairs are nonzero and do not define an analytical zero-angle oracle. + +## Canonical oracle output + +| DataPath | Source of expected values | +|---|---| +| `/triangle_geom/FaceData/NXFaceMisorientationColors` | Class 1 symmetry reduction: 33 literal angles and four NaN checks in `Curated Data`. | +| `/Bounds Data/Bounds Misorientations` | Mixed phases give NaN; a shared out-of-range phase returns `-98412` before angle calculation. | + +For each class, reduce a pure c-axis rotation by its allowed rotational symmetries. The expected angles below are in degrees. This table preserves the analytical derivation from the report and matches the test literals. + +| Laue class | Index | 45° input | 90° input | 180° input | +|---|---:|---:|---:|---:| +| Hexagonal_High | 0 | 15 | 30 | 0 | +| Cubic_High | 1 | 45 | 0 | 0 | +| Hexagonal_Low | 2 | 15 | 30 | 0 | +| Cubic_Low | 3 | 45 | 90 | 0 | +| Triclinic | 4 | 45 | 90 | 180 | +| Monoclinic | 5 | 45 | 90 | 180 | +| OrthoRhombic | 6 | 45 | 90 | 0 | +| Tetragonal_Low | 7 | 45 | 0 | 0 | +| Tetragonal_High | 8 | 45 | 0 | 0 | +| Trigonal_Low | 9 | 45 | 30 | 60 | +| Trigonal_High | 10 | 45 | 30 | 60 | + +The curated angle checks require an absolute error below 0.000012°. Faces 30–33 use `std::isnan`. Thus the test checks 37 face values in total, not 37 angles plus four NaN values. The cubic 90° case must reduce to zero because that rotation is a cubic symmetry operation. The Trigonal_Low and Trigonal_High fixtures both reduce by 120° rotations about c. + +## Oracle provenance (Classes 2, 3, 5 only) + +Not applicable. The correctness oracle is Class 1 and lives in the test source. The legacy and local proof outputs serve only as comparison evidence. + +The report records Linux, macOS and Windows in-core CI passes for the three ordinary test cases at `419f9db35`. This CI statement does not include the hidden boundary fixture. + +## Second-engineer oracle review + +- **Reviewer:** Michael Jackson, as recorded in the report's Oracle and Sign-off fields. +- **Date:** 2026-05-28. + +## Regenerated to fix a circular-oracle situation? + +The inline analytical expectations replace the former regression against `6_6_Small_IN100_GBCD.tar.gz`. PR #1618 removes that archive consumer when the output changes from three components to one. PR #1631 changes the cubic 90° expectation from 0.021200536° to the analytical zero and adds Trigonal_High. No current correctness expectation comes from a regenerated SIMPLNX output archive. diff --git a/src/Plugins/OrientationAnalysis/vv/provenance/ComputeFeatureFaceMisorientations.md b/src/Plugins/OrientationAnalysis/vv/provenance/ComputeFeatureFaceMisorientations.md deleted file mode 100644 index cc5714819a..0000000000 --- a/src/Plugins/OrientationAnalysis/vv/provenance/ComputeFeatureFaceMisorientations.md +++ /dev/null @@ -1,59 +0,0 @@ -# Exemplar Archive Provenance: Inlined in Test - -This sidecar records how the test data used by `ComputeFeatureFaceMisorientationFilter`'s Class 1 unit test was generated. It is the answer to "where did this hand-built data come from?" - -The test data is **inlined** in the test source — there is no separate tar.gz archive, no `download_test_data()` entry, and no `.dream3d` exemplar file to fetch. - ---- - -## Archive identity - -| Field | Value | -|-------------------|----------------------------------------------------------------------------------------------------------------| -| **Archive** | Inlined (no separate archive) | -| **SHA512** | N/A | -| **Used by tests** | `OrientationAnalysis::ComputeFeatureFaceMisorientationFilter: Curated Data` | -| **Generated by** | Michael Jackson (dataset structure + first 10 Laue classes) — Claude (Trigonal_High extension, 2026-05-28) | -| **Generated on** | 2026-05-01 (initial dataset); 2026-05-28 (Trigonal_High added) | - -## How it was generated - -The dataset is a hand-rolled triangulated mesh designed as a **Class 1 (Analytical) oracle** that systematically covers all 11 EbsdLib Laue classes plus all four invalid-face scenarios: - -1. **Mesh structure:** 102 + 9 = 111 vertices, arranged in y-axis-stacked blocks of 9 vertices per "row" (one row per Laue class block); 34 + 3 = 37 triangles, each with its own 3 dedicated vertices (no shared edges or vertices between triangles). The algorithm reads only `FaceLabels`, not vertex coordinates, so the geometric layout is arbitrary; the row-stacked structure is purely for human readability. - -2. **Feature structure:** 41 + 4 = 45 features (indices 0–44). Feature 0 is a sentinel/unused (phase `999`). Features 1–4 are phase 1 (Hex_High), 5–8 phase 2 (Cubic_High), ..., 37–40 phase 10 (Trigonal_Low), 41–44 phase 11 (Trigonal_High, added). - -3. **Ensemble structure:** 12 + 1 = 13 phase entries (index 0 = sentinel `999`; indices 1–11 = Laue-class indices 0–10). Phase `i` has `CrystalStructure[i] = i - 1` for `i = 1..11`, so phase 1 → CrystalStructure 0 (Hex_High), phase 2 → CrystalStructure 1 (Cubic_High), ..., phase 11 → CrystalStructure 10 (Trigonal_High). - -4. **Orientations (`AvgEulerAngles`):** Each Laue class block uses four orientations: `(φ1, Φ, φ2) = (0°, 0°, 0°)`, `(45°, 0°, 0°)`, `(90°, 0°, 0°)`, `(180°, 0°, 0°)`. With `Φ = φ2 = 0`, these are pure rotations about the c-axis. The choice is deliberate: pure c-axis rotations allow closed-form symmetry-group reduction (the c-axis is the principal symmetry axis of each Laue class), so the expected misorientation is just `|Δφ1|` modulo the c-axis n-fold rotation. - -5. **Face labels:** 34 + 3 = 37 entries. The first 30 entries are organized as Laue-class blocks of 3 boundary types: A↔B (φ1 0° vs 45°), A↔C (0° vs 90°), A↔D (0° vs 180°). Entries 30–33 are the four invalid-face edge cases: (0, 1) background-front, (1, 0) background-back, (1, 5) different-phase forward, (5, 1) different-phase reverse. Entries 34–36 are the Trigonal_High boundaries added. - -6. **Expected values:** Derived in closed form from the c-axis n-fold rotation of each Laue class. See the V&V report's Oracle table for the full per-class derivation. The Trigonal_High values (45°, 30°, 60°) match Trigonal_Low — the additional mirror planes in -3m do not reduce pure c-axis rotation magnitudes further. - -7. **Conversion path in the test:** The Euler angles are stored in degrees as `AvgEulerAngles`. The test first runs `ChangeAngleRepresentationFilter` (degrees → radians), then `ConvertOrientationsFilter` (Euler → quaternion, writing `AvgQuats`), then `ComputeFeatureFaceMisorientationFilter` itself. This conversion chain mirrors the standard SIMPLNX EBSD pipeline, so the test exercises the real production code path for converting hand-specified Euler angles into the quaternion format the algorithm consumes. - -The dataset is inlined in `src/Plugins/OrientationAnalysis/test/ComputeFeatureFaceMisorientationTest.cpp` namespace `curated` (function `CreateTestDataStructure()`). - -## Canonical oracle output - -| DataPath | Source of expected values | -|----------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `/triangle_geom/FaceData/NXFaceMisorientationColors` | Class 1 analytical (closed-form symmetry-group reduction). Hand-derived per Laue class; see the V&V report's "Applied (Class 1)" table for the full per-class values and per-face expected misorientation. | - -The expected values are hard-coded into the test as `REQUIRE(::CompareFloats(faceMisorientations[i], f))` checks (37 such asserts + `std::isnan` checks for the 4 edge cases). - -## Oracle provenance (Classes 2, 3, 5 only) - -N/A — Class 1 oracle. - -## Second-engineer oracle review - -- **Reviewer:** *Pending — recommend Joey Kleingers or another OA-domain engineer review the symmetry-group hand-calculations for all 11 Laue classes (especially the Trigonal_High → Trigonal_Low equivalence claim and the Cubic_High F5↔F7 → 0° claim post-EbsdLib-precision-fix).* -- **Date:** *YYYY-MM-DD (pending)* -- **Skip reason** (if skipped): *N/A — second-engineer review is recommended; not yet performed.* - -## Regenerated to fix a circular-oracle situation? - -N/A. This dataset is brand-new for the SIMPLNX V&V cycle; no prior exemplar existed for this filter that needed retroactive replacement. (The pre-rewrite test used the unrelated `6_6_Small_IN100_GBCD.tar.gz` archive as a regression-against-reference exemplar; that test was retired when the algorithm was rewritten to a 1-component output that's structurally incompatible with the old archive's 3-component reference.) diff --git a/src/Plugins/OrientationAnalysis/vv/provenance/ComputeFeatureNeighborCAxisMisalignmentsFilter.md b/src/Plugins/OrientationAnalysis/vv/provenance/ComputeFeatureNeighborCAxisMisalignmentsFilter.md index 0a910cbf04..56380b023b 100644 --- a/src/Plugins/OrientationAnalysis/vv/provenance/ComputeFeatureNeighborCAxisMisalignmentsFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/provenance/ComputeFeatureNeighborCAxisMisalignmentsFilter.md @@ -1,254 +1,131 @@ -# Exemplar Archive Provenance: Inlined in Test +# Exemplar Archive Provenance: compute_feature_neighbor_caxis_misalignments.tar.gz -This sidecar records how the test data used by `ComputeFeatureNeighborCAxisMisalignmentsFilter`'s Class 1 and Class 4 unit tests was generated. It is the answer to "where did this hand-built data come from?" - -The test data is **inlined** in the test source — there is no separate tar.gz archive, no `download_test_data()` entry, and no `.dream3d` exemplar file to fetch. - ---- +This sidecar records the registered archive and the independent inline fixtures used by `ComputeFeatureNeighborCAxisMisalignmentsFilter`. ## Archive identity -| Field | Value | -|-------------------|------------------------------------------------------------------------------------------------------------------------------------------------| -| **Archive** | Inlined (no separate archive) | -| **SHA512** | N/A | -| **Used by tests** | `OrientationAnalysis::ComputeFeatureNeighborCAxisMisalignmentsFilter: Class 1 - Simple Hex Pair` | -| | `OrientationAnalysis::ComputeFeatureNeighborCAxisMisalignmentsFilter: Class 1 - Realistic Microstructure (exposes divisor bug)` | -| | `OrientationAnalysis::ComputeFeatureNeighborCAxisMisalignmentsFilter: Class 1 - Mismatch Last Order` | -| | `OrientationAnalysis::ComputeFeatureNeighborCAxisMisalignmentsFilter: Class 4 - Invariants` (3 SECTIONs) | -| **Generated by** | Claude (Opus 4.7, Anthropic) under direction of Michael Jackson | -| **Generated on** | 2026-06-04 | +| Field | Value | +|---|---| +| **Archive** | `compute_feature_neighbor_caxis_misalignments.tar.gz` | +| **SHA512** | `955cd35b7ae24579ef9c533df34e1118012a8e5e2a71f8613117c714fc220c5dfa78d91a2964b41752e70684b79d4aa790e488e9a7be4c9dcf7b642ee2897ceb` | +| **Used by tests** | None in the current filter test file. PR #1631 removed `OrientationAnalysis::ComputeFeatureNeighborCAxisMisalignmentsFilter: Valid Filter Execution`. | +| **Generated by** | Michael Jackson (inferred from PR #1467) | +| **Generated on** | — | +| **Generated at commit** | `963f8d64e` (PR #1467 merge, inferred) | ---- +`src/Plugins/OrientationAnalysis/test/CMakeLists.txt` still registers this archive and SHA512. The first registration is PR #1467 (`963f8d64e`). The current tests use inline inputs instead of this archive. -## Retired archive (replaced by this inlined fixture) +The registration commit is dated 2025-11-12. That date records the addition of the archive registration, not archive generation or the PR opening date. -| Field | Value | -|-------------------|------------------------------------------------------------------------------------| -| **Archive name** | `compute_feature_neighbor_caxis_misalignments.tar.gz` | -| **SHA512** | `955cd35b7ae24579ef9c533df34e1118012a8e5e2a71f8613117c714fc220c5dfa78d91a2964b41752e70684b79d4aa790e488e9a7be4c9dcf7b642ee2897ceb` | -| **Contents** | `7_5_simplnx_test_file_25x50_Hex.dream3d` — a hex-phase-only dataset, with exemplar `CAxisMisalignmentList (7_5)` and `AvgCAxisMisalignments (7_5)` arrays generated from a SIMPL 6.5.171 pipeline run. | -| **Retired on** | 2026-06-04 | -| **Retired by** | This V&V cycle | -| **Reason** | The exemplar dataset is **hex-phase-only**, which means the per-mismatch decrement branch in the algorithm (`hexNeighborListSize--`) is never exercised. The exemplar values would have happily passed even on the divisor-bug code (D1). The hex-only exemplar therefore could not have caught the D1 bug, and was a circular oracle in the more general sense (the legacy bug pattern is invisible on the dataset). Replaced with the inlined Class 1 + Class 4 fixtures below, which include 3 distinct bug-exposing per-feature configurations. | -| **CMakeLists.txt**| `download_test_data(... compute_feature_neighbor_caxis_misalignments.tar.gz ...)` removed from `src/Plugins/OrientationAnalysis/test/CMakeLists.txt` (no other filter test consumed this archive). | +## How it was generated ---- +PR #1467 added a regression test that reads `compute_feature_neighbor_caxis_misalignments/7_5_simplnx_test_file_25x50_Hex.dream3d`. That test compared its computed neighbor list and average array with the stored arrays named `CAxisMisalignmentList (7_5)` and `AvgCAxisMisalignments (7_5)`. PR #1631 (`ec22ac375`) replaced the archive consumer with synthetic fixtures constructed in `src/Plugins/OrientationAnalysis/test/ComputeFeatureNeighborCAxisMisalignmentsTest.cpp`. These fixtures use literal orientations, phase assignments, neighbor lists, and expected values. -## How the inlined fixtures were generated +The inline fixture builders are `AnalyticalFixtures::CreateScaffold`, `QuatFromPhiDeg`, `SetAvgQuat`, `BuildArgs`, and `BuildRealisticMicrostructure`. `CreateScaffold` creates an image geometry, cell arrays, feature arrays, and an ensemble array. The algorithm reads feature and ensemble inputs; it does not read the cell geometry or cell-level feature map. -The dataset is a hand-rolled in-memory `DataStructure` designed as a **Class 1 (Analytical) oracle** with a paired **Class 4 (Invariant)** check. It systematically covers the six algorithmic paths in `ComputeFeatureNeighborCAxisMisalignments::operator()()`: +| Group | Arrays | Initial values | +|---|---|---| +| `/ImageGeometry/CellData` | `FeatureIds`, `Phases` | One; the mixed fixture replaces them with its feature map. | +| `/ImageGeometry/CellFeatureData` | `FeaturePhases`, `AvgQuats`, `NeighborList` | Phase zero, identity quaternion, and empty list. | +| `/ImageGeometry/CellEnsembleData` | `CrystalStructures` | Tuple zero is sentinel 999; each fixture assigns its real ensembles. | -1. **All-non-hex preflight early-exit** → error -1562 (no hex phases). NOT exercised by the V&V fixtures (all contain at least one hex phase) — covered by upstream parameter-validation tests. -2. **Mixed-phase preflight warning** → warning -1563 ("Non Hexagonal phases were found"). -3. **Per-feature outer loop with hex-hex same-phase neighbor** → write angle to misoList + accumulate to avg. -4. **Phase-mismatch branch** → write NaN to misoList + decrement divisor. -5. **`find_avg_misals=true` finalize with `hexNeighborListSize > 0`** → `avg = sum/divisor`. -6. **`find_avg_misals=true` finalize with `hexNeighborListSize == 0`** → `avg = NaN`. +The feature-level inputs break feature and phase symmetry through different tilts, neighbor counts, and crystal structures. The cell layout is a visualization aid. Its equal X and Y dimensions and zero origin do not enter this algorithm. -### Scaffold structure +**Simple Hex Pair** -The `AnalyticalFixtures` namespace at the top of `ComputeFeatureNeighborCAxisMisalignmentsTest.cpp` provides a `CreateScaffold(nX, nY, nZ, numFeatures, numCrystalStructures)` helper that constructs: +The scaffold has a 1×1×1 image and three feature tuples, including background. Features 1 and 2 have Hexagonal_High structure, tilts 0° and 10°, and reciprocal neighbor entries. Both pair angles and both averages must be 10°. -- A single `ImageGeom` named `ImageGeometry` with the requested cell dimensions, spacing `{1, 1, 1}`, origin `{0, 0, 0}`. -- A `CellData` `AttributeMatrix` with tuple shape `{nZ, nY, nX}` for cell-level arrays: `FeatureIds` (int32, default 1), `Phases` (int32, default 1). **These cell-level arrays are NOT consumed by the algorithm** — they exist purely as a visualization aid so engineers reviewing the fixture can see which voxels belong to which feature. The algorithm's inputs are feature-level (`FeaturePhases`, `AvgQuats`, `NeighborList`) and ensemble-level (`CrystalStructures`). -- A `CellFeatureData` `AttributeMatrix` with tuple shape `{numFeatures}` for feature-level arrays: `FeaturePhases`, `AvgQuats` (4 components), `NeighborList`. Defaults: all phase 0, identity quat, empty neighbor list. -- A `CellEnsembleData` `AttributeMatrix` with tuple shape `{numCrystalStructures}` for `CrystalStructures` (UInt32Array). Defaults: index 0 = sentinel `999u`; caller sets other indices. +**Realistic Microstructure** -Helpers: -- `QuatFromPhiDeg(phiDeg)` produces `{sin(phi/2 rad), 0, 0, cos(phi/2 rad)}` — the quaternion for a pure Bunge ZXZ `(φ1, Φ, φ2) = (0, phiDeg, 0)` rotation. See "Orientation convention" below. -- `SetAvgQuat(td, featureIdx, q)` writes a quaternion into the `AvgQuats` array. -- `BuildArgs(findAvgMisals)` constructs the `Arguments` object for the filter with the standard input/output paths. -- `BuildRealisticMicrostructure()` constructs the 10×10×1 6-feature fixture used by Fixture 2 (and reused by Fixture 4 / Class 4 Invariants). +The scaffold has a 10×10×1 image and seven feature tuples, including background. Features F1, F2, F4, F5, and F6 use phase 1, whose structure is Hexagonal_High. F3 uses phase 2, whose structure is Cubic_High. -### Orientation convention +| Feature | Tilt | Neighbor list | +|---|---|---| +| F1 | 0° | F2, F4 | +| F2 | 5° | F1, F3, F4, F5 | +| F3 | 10° | F2, F5, F6 | +| F4 | 15° | F1, F2, F5 | +| F5 | 20° | F2, F3, F4, F6 | +| F6 | 25° | F3, F5 | -All Class 1 fixtures use **pure Bunge ZXZ Euler rotations `(0, Φ, 0)`** — rotations about the x-axis by Φ degrees. Closed-form derivation: +The cell layout that defines these face adjacencies is: -The orientation matrix for a pure-Φ rotation about x is: -``` -R(Φ) = [[1, 0, 0], - [0, cos(Φ), -sin(Φ)], - [0, sin(Φ), cos(Φ)]] +```text +y = 0..3: x = 0..2 -> F1 | x = 3..6 -> F2 | x = 7..9 -> F3 +y = 4..9: x = 0..3 -> F4 | x = 4..7 -> F5 | x = 8..9 -> F6 ``` -The algorithm computes `c = R^T · [0, 0, 1] = [0, sin(Φ), cos(Φ)]` — the c-axis vector, tilted from the global z-axis by Φ degrees. - -For two features with tilts `Φ_A` and `Φ_B`: -- `c_A · c_B = sin(Φ_A) sin(Φ_B) + cos(Φ_A) cos(Φ_B) = cos(Φ_A - Φ_B)` -- `arccos(c_A · c_B) = |Φ_A - Φ_B|` -- Folded to `[0, π/2]` via the algorithm's `if(w > π/2) w = π - w` step: still `|Φ_A - Φ_B|` as long as `|Φ_A - Φ_B| ≤ 90°` (true for all V&V fixtures, which use tilts in `[0°, 25°]`). - -This makes the oracle closed-form: the expected c-axis misalignment between two hex-phase features with pure-Φ tilts is exactly `|Φ_A - Φ_B|` degrees, with no numerical sym-op search required. +F2/F4 and F3/F5 each share one cell face across the y=3/y=4 boundary. They are face adjacencies, not diagonal-only contacts. The algorithm consumes the supplied neighbor lists and does not derive them from the cell map. -### Fixture-by-fixture derivation +**Mismatch Last Order** -#### Fixture 1 — `Class 1 - Simple Hex Pair` +The scaffold has a 1×1×1 image and five feature tuples. F1, F2, and F3 have Hexagonal_High structure and tilts 0°, 5°, and 10°. F4 has Cubic_High structure. F1's neighbor list is [F2, F3, F4], so its output is [5°, 10°, NaN] and its average is 7.5°. The old divisor also gives 7.5° for this ordering; this is a control case. -- 1×1×1 image, 3 features total (sentinel + 2 hex). -- `(*td.crystalStructures)[1] = ebsdlib::CrystalStructure::Hexagonal_High`. -- F1: `phase=1, Φ=0°`. F2: `phase=1, Φ=10°`. -- NeighborList: F1→[F2], F2→[F1]. -- Expected: `misalignmentList[F1] = [10°]`, `misalignmentList[F2] = [10°]`, `avg[F1] = avg[F2] = 10°`. +**Class 4 invariants** -This is the basic-path test — no phase mismatches, no divisor decrements. Confirms the closed-form `|ΔΦ|` derivation and the hex-hex accumulator path. +The invariant test reuses the realistic fixture. Its three sections check the finite-angle range [0°, 90°], the average of the non-NaN list entries, and NaN outputs for the cubic focal feature F3. The mean invariant detects the divisor defect independently of the chosen tilt values. -#### Fixture 2 — `Class 1 - Realistic Microstructure (exposes divisor bug)` +**Quaternion boundary fixture** -The meaty fixture. 10×10×1 image, 7 features (sentinel + 6 real). +The hidden `genuine HDF5 1MiB-chunk tail oracle` constructs 65,537 feature quaternions. It asserts a 65,536-tuple chunk shape and assigns tilts 15° and 30° to the last tuple of the full chunk and the first tuple of the tail. Both features point to identity feature 1. The expected pair angles and averages are 15° and 30°. This fixture uses a `DataGroup` with the same feature and ensemble paths; no image geometry is needed. -**Phase assignment:** -- `(*td.crystalStructures)[1] = Hexagonal_High` -- `(*td.crystalStructures)[2] = Cubic_High` -- F1, F2, F4, F5, F6 → phase 1 (Hex) -- F3 → phase 2 (Cubic, non-hex) +**Validation fixtures** -**Orientation assignment (pure Φ tilts about x):** -- F1: Φ=0°, F2: Φ=5°, F3: Φ=10° (ignored — non-hex), F4: Φ=15°, F5: Φ=20°, F6: Φ=25°. - -**Cell-by-cell FeatureIds layout** (rows=y, cols=x): - -``` -y=0..3: x=0..2 → F1 | x=3..6 → F2 | x=7..9 → F3 -y=4..9: x=0..3 → F4 | x=4..7 → F5 | x=8..9 → F6 -``` +The tuple-count fixture supplies eight neighbor and quaternion tuples but seven phase tuples, so preflight must return `-1560`. The phase-index fixture uses values -1 and 2 with two ensemble tuples. It expects `-1564` for either a focal or neighbor lookup and permits an invalid phase on unused background feature zero. -This produces the following face-adjacencies (face-adjacent = two cells sharing a 1-cell edge): - -- F1 ↔ F2 (at x=2/x=3 boundary, y=0..3 — 4 face-shared cells) -- F1 ↔ F4 (at y=3/y=4 boundary, x=0..2 — 3 face-shared cells) -- F2 ↔ F3 (at x=6/x=7 boundary, y=0..3 — 4 face-shared cells) -- F2 ↔ F4 (at (x=3, y=3)/(x=3, y=4) — 1 face-shared cell — corner adjacency) -- F2 ↔ F5 (at y=3/y=4 boundary, x=4..6 — 3 face-shared cells) -- F3 ↔ F5 (at (x=7, y=3)/(x=7, y=4) — 1 face-shared cell) -- F3 ↔ F6 (at y=3/y=4 boundary, x=8..9 — 2 face-shared cells) -- F4 ↔ F5 (at x=3/x=4 boundary, y=4..9 — 6 face-shared cells) -- F5 ↔ F6 (at x=7/x=8 boundary, y=4..9 — 6 face-shared cells) - -**NeighborList entries** (face-adjacencies derived from the layout above): - -- F1: [F2, F4] -- F2: [F1, F3, F4, F5] -- F3: [F2, F5, F6] -- F4: [F1, F2, F5] -- F5: [F2, F3, F4, F6] -- F6: [F3, F5] - -**Per-feature expected outputs** (post-fix algorithm): - -| Feature | `misalignmentList[F]` | divisor | sum | avg (post-fix) | avg (pre-fix bug) | -|---------|--------------------------|---------|-----|----------------|-------------------| -| F1 | [5°, 15°] | 2 | 20° | **10.000°** | 10.000° (no mismatches → no divisor change) | -| F2 | [5°, NaN, 10°, 15°] | 3 | 30° | **10.000°** | **7.500° (30/4)** ← bug-exposing | -| F3 | [NaN, NaN, NaN] | 0 | — | **NaN** | NaN (non-hex focal) | -| F4 | [15°, 10°, 5°] | 3 | 30° | **10.000°** | 10.000° (no mismatches) | -| F5 | [15°, NaN, 5°, 5°] | 3 | 25° | **8.3333°** | **6.250° (25/4)** ← bug-exposing | -| F6 | [NaN, 5°] | 1 | 5° | **5.000°** | **2.500° (5/2)** ← bug-exposing | - -F2, F5, F6 each have at least one non-hex neighbor followed by at least one hex neighbor. The pre-fix algorithm reassigned `hexNeighborListSize` to the full list size on every j-iteration, so the per-mismatch decrement at line 150 was clobbered by the next iteration's reassignment, leaving the final divisor equal to the full neighbor-list length instead of the hex-only neighbor count. Post-fix code moves the assignment to before the inner loop, so the decrement is preserved. - -#### Fixture 3 — `Class 1 - Mismatch Last Order` (Control case) +## Canonical oracle output -- 1×1×1 image, 5 features (sentinel + 4 real). -- Phases: F1, F2, F3 hex (Φ=0°, 5°, 10°), F4 cubic (Φ=20° — ignored). -- NeighborList: F1 → [F2, F3, F4] — order [match, match, mismatch]. -- Expected: `misalignmentList[F1] = [5°, 10°, NaN]`, divisor=2, sum=15°, avg=7.500°. -- **Pre-fix code also produces 7.500°** because the last j-iteration is a non-hex match → the reassignment runs (hexNeighborListSize=3) then the decrement runs (hexNeighborListSize=2). Bug doesn't fire on this ordering. +The canonical values are defined in the C++ test source. The registered archive is not the correctness oracle. -Control fixture — confirms that the post-fix code does NOT regress the case where the buggy code happened to produce the right answer by accident. +| DataPath | Source of expected values | +|---|---| +| `/ImageGeometry/CellFeatureData/CAxisMisalignmentList` | Class 1: folded absolute tilt differences for equal accepted hexagonal structures; NaN for excluded pairs. | +| `/ImageGeometry/CellFeatureData/AvgCAxisMisalignments` | Class 1 arithmetic means and Class 4 checks of the mean of non-NaN entries. No accepted entries gives NaN. | -#### Fixture 4 — `Class 4 - Invariants` (3 SECTIONs, reuses Fixture 2 data) +For a pure x-axis rotation by Φ, the quaternion is `(sin(Φ/2), 0, 0, cos(Φ/2))`. Applying the transposed orientation matrix to crystal [001] tilts the c-axis by Φ. Two such axes have dot product `cos(Φ_A - Φ_B)`. Their unoriented-axis angle is the absolute tilt difference folded into [0°, 90°]. This gives the expectations without calling the production orientation-matrix code. -Three SECTIONs all using the realistic-microstructure fixture: +The realistic fixture gives: -**Sub-section (i) — Range:** every `misalignmentList[F][j]` is either NaN or in `[0°, 90°]`. The 90° upper bound is enforced by the algorithm's `if(w > π/2) w = π - w` fold; the 0° lower bound by `arccos` returning non-negative. +| Feature | Pair angles | Accepted count | Sum | Expected average | +|---|---|---|---|---| +| F1 | [5°, 15°] | 2 | 20° | 10° | +| F2 | [5°, NaN, 10°, 15°] | 3 | 30° | 10° | +| F3 | [NaN, NaN, NaN] | 0 | 0° | NaN | +| F4 | [15°, 10°, 5°] | 3 | 30° | 10° | +| F5 | [15°, NaN, 5°, 5°] | 3 | 25° | 25/3° | +| F6 | [NaN, 5°] | 1 | 5° | 5° | -**Sub-section (ii) — Per-feature averaging formula:** for each feature `F`, `avg[F] == sum(non-NaN entries in misalignmentList[F]) / count(non-NaN entries in misalignmentList[F])`, or `NaN` if count == 0. **This is the load-bearing invariant for D1.** Under the pre-fix code, F2, F5, F6 fail this invariant (their stored avg does not equal the formula recomputed from the per-neighbor list). Under the post-fix code, all features pass. +The test checks all 18 list entries and six averages. Finite values use an absolute tolerance of 0.001°; NaN values use `std::isnan`. F2, F5, and F6 expose the lost divisor decrements. F3 distinguishes a zero average from the required NaN when all three neighbors are excluded. -**Sub-section (iii) — Non-hex focal → all NaN:** every entry in `misalignmentList[F3]` is NaN, and `avg[F3]` is NaN. F3 has Cubic_High phase, so the `xtalPhase1 == ebsdlib::CrystalStructure::Hexagonal_*` gate fails for every neighbor, regardless of the neighbor's phase. +No fixture tests an angle above 90° or selects Hexagonal_Low. The range invariant does not exercise the folding branch. The report lists these coverage gaps separately. -Class 4 invariants are oracle-agnostic — they hold for any input regardless of specific Φ values. This is what makes them a robust complement to the Class 1 fixtures: they would catch regression of the divisor bug even if the specific orientation values in Fixture 2 were changed. +**Release-source evidence** -## Canonical oracle output +All ten `upstream/release/v` branches from v7.0.0 through v7.4.2 were inspected at `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeFeatureNeighborCAxisMisalignments.cpp`. The preflight-channel comparison uses `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeFeatureNeighborCAxisMisalignmentsFilter.cpp` on those same branches. -| DataPath | Source of expected values | -|-------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------| -| `/ImageGeometry/CellFeatureData/CAxisMisalignmentList` | Class 1 analytical (closed-form `|ΔΦ|` for pure-Φ rotations, folded to `[0°, 90°]`; NaN on phase mismatch or non-hex Laue class). | -| `/ImageGeometry/CellFeatureData/AvgCAxisMisalignments` | Class 1 analytical (arithmetic mean of non-NaN entries from above) + Class 4 invariant (range, formula, non-hex-focal). | +| Entry | Release-source result | Current implementation | +|---|---|---| +| D1 | v7.0.0 through v7.4.1 reset the divisor inside the neighbor loop. v7.4.2 initializes it before the loop. | PR #1631 (`ec22ac375`) supplies the correction. | +| D2 | v7.0.0 through v7.4.2 accumulate directly in the output array. | PR #1568 (`4ec21e93f`) initializes an average buffer with 0.0f. | +| D5 | v7.0.0 through v7.2.2 return the general notice as a preflight warning. v7.4.0 through v7.4.2 return it as an updated value. | PR #1438 (`e6896714b`) changes the preflight channel; execution warning `-1563` remains. | +| D6 | v7.0.0 through v7.4.2 accept equal Hexagonal_High or equal Hexagonal_Low structures. | Both classes remain accepted. | +| D7 | v7.0.0 through v7.4.2 read Crystal Structures through unchecked phase indices. | PR #1568 (`4ec21e93f`) adds focal and neighbor bounds checks. | -The expected values are hard-coded into each TEST_CASE as `REQUIRE(... == Approx(...).margin(1e-3f))` checks (per-pair) and `REQUIRE(std::isnan(...))` checks (per NaN entry). Tolerance set to `1e-3°` — tight enough to catch the divisor bug's `1.667°` minimum magnitude error on the Realistic Microstructure fixture, comfortably wider than any expected float32 precision noise from the Eigen orientation-matrix arithmetic. +The DREAM.3D v6.5.171 source (tag `v6.5.171`) at `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/FindFeatureNeighborCAxisMisalignments.cpp` contains the D1 divisor reset and D7 unchecked phase lookups. It explicitly initializes the average output to zero, uses float matrix calculations, and accepts only Hexagonal_High pairs. Its data check validates tuple counts but does not validate phase values. These source facts establish the legacy effects described in the deviations sidecar. ## Oracle provenance (Classes 2, 3, 5 only) -N/A — Class 1 and Class 4 oracles only. No reference-library invocation, no paper-figure reproduction, no expert-visual sign-off needed. +Not applicable. The correctness oracles are Class 1 and Class 4, with derivations and assertions in the test source. ## Second-engineer oracle review -- **Reviewer:** *Pending — recommend Joey Kleingers or another OA-domain engineer review (a) the realistic-microstructure F2/F5/F6 per-feature expected averages, (b) the closed-form derivation that pure Bunge ZXZ `(0, Φ, 0)` tilts the c-axis by exactly Φ degrees, and (c) the cell-by-cell face-adjacency derivation that produced the per-feature NeighborList entries.* -- **Date:** *YYYY-MM-DD (pending)* -- **Skip reason** (if skipped): *N/A — second-engineer review is recommended; not yet performed.* - -## Empirical A/B validation (2026-06-04) - -After the V&V cycle's source-inspection comparison + analytical Class 1 fixtures, an **empirical A/B comparison** was performed against three binaries to validate every deviation claim end-to-end. Workspace at `/Users/mjackson/Desktop/F6_AB_Test/`: - -| Binary | Path | Role | -|--------|-------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------| -| A | `/Users/mjackson/Applications/DREAM3D.app/Contents/bin/PipelineRunner` | DREAM3D 6.5.171 official release — pre-fix, buggy | -| B | `/Users/mjackson/DREAM3D-Dev/DREAM3D-Build/D3D-Rel-Qt515-6_5_171/Bin/PipelineRunner` | Local build of the DREAM3D 6.5.171 source with the surgical divisor fix applied — used to prove the root cause | -| C | `/Users/mjackson/Workspace9/DREAM3D-Build/NX-Com-Qt69-Vtk95-Rel-EbsdLib/Bin/nxrunner` | SIMPLNX (post-fix), linked against EbsdLib 2.4.1 build | - -**Input:** `input/build_input.py` generates `input/f6_realistic_microstructure.dream3d` — a legacy v7.0-format `.dream3d` file containing the realistic-microstructure fixture (10×10×1 ImageGeom, 6 features, mixed hex/non-hex phases, pure-Φ Bunge ZXZ rotations). The Python script is fully reproducible — anyone can re-derive the input. - -**Pipelines:** `pipelines/legacy_6_5_171.json` (legacy SIMPL JSON format), a second legacy pipeline for the patched build (identical except output path), `pipelines/simplnx.d3dpipeline` (SIMPLNX format). All three pipelines: read the input → run the filter with `find_avg_misals=true` → write output. - -**Comparison:** `notes/compare.py` reads all three output `.dream3d` files and prints per-feature side-by-side comparisons. Results captured in `notes/ab_results.txt`. - -**Key findings (validated against the analytical Class 1 expected values):** - -| Feature | Expected | Pre-fix bug prediction | 6.5.171 (actual) | Patched legacy (actual) | SIMPLNX (actual) | -|---------|----------|------------------------|------------------|------------------|------------------| -| F1 | 10.0000° | 10.0000° (dormant) | 10.0000° | 10.0000° | 10.0000° | -| F2 | 10.0000° | 7.5000° (bug fires) | **7.5000°** ❌ | 10.0000° ✓ | 10.0000° ✓ | -| F3 | NaN | NaN | **0.0000** ❌ | NaN ✓ | NaN ✓ | -| F4 | 10.0000° | 10.0000° (dormant) | 10.0000° | 10.0000° | 10.0000° | -| F5 | 8.3333° | 6.2500° (bug fires) | **6.2500°** ❌ | 8.3333° ✓ | 8.3333° ✓ | -| F6 | 5.0000° | 2.5000° (bug fires) | **2.5000°** ❌ | 5.0000° ✓ | 5.0000° ✓ | - -Per-pair `CAxisMisalignmentList` values are functionally identical across all three binaries within float32 precision (~`1e-6°`). The drift between the legacy MatrixMath float32 path and SIMPLNX (Eigen double-precision path) is the empirical magnitude of D4. - -**Empirical conclusions about each deviation:** - -- **D1 confirmed**: bug fires on 6.5.171 producing exactly the predicted-buggy values; fixed in the patched local build of the legacy source AND in SIMPLNX. **Bonus observation**: F3's non-hex-only neighbor case produces `0.0` on 6.5.171 instead of `NaN` — see D1 entry's "Additional symptom" paragraph. -- **D2 dormant**: F1's exact `10.0000` proves the SIMPLNX `AvgCAxisMisalignments` array IS zero-initialized by the current in-memory DataStore default. The latent bug doesn't fire on the current backend. -- **D4 quantified (pre-fix) and then closed (post-fix)**: pre-fix drift was `~1e-6°` per-pair / `~2e-5°` per-feature avg. Existing doc note's `~0.0001°` estimate is ~100× too high. The Eigen + double precision conversion was surgically applied to a local build of the legacy source, following the `FindAvgCAxes` and `FindFeatureReferenceCAxisMisorientations` legacy-patch precedents. **Post-fix: the patched legacy build produces BIT-IDENTICAL output to SIMPLNX** — all 18 per-pair entries + 6 per-feature entries byte-compared via h5py and confirmed identical. -- **D5 partially retracted**: empirical A/B confirmed SIMPLNX nxrunner DOES emit the algorithm-level "Non Hexagonal phases" warning at execute-time. The PR #1438 regression was specifically the *preflight-time* GUI banner, not the warning channel as originally claimed. -- **D6 added 2026-06-04**: Hexagonal_Low support gap surfaced via source-inspection during the post-A/B precedent search. Not observable on the F#6 fixture (no Hex_Low features) but a real behavior gap on wurtzite-class data. **Fixed in the patched local build of the legacy source** bundled with D4 (the Eigen conversion patch also lifted the Hex_High-only gate to accept both Hex Laue classes — same gate location, single patch). - -## Post-D4-fix byte-for-byte verification - -After applying the surgical fix (Eigen + double + Hex_Low) to the local build of the legacy source, re-ran the A/B comparison via direct h5py byte-comparison (`B` = patched legacy build): - -``` -AvgCAxisMisalignments byte-by-byte diff: - F1: B=10.0 NX=10.0 byte-match=True - F2: B=10.0 NX=10.0 byte-match=True - F3: B=nan NX=nan byte-match=True - F4: B=10.0 NX=10.0 byte-match=True - F5: B=8.333335 NX=8.333335 byte-match=True - F6: B=5.0 NX=5.0 byte-match=True - -CAxisMisalignmentList byte-by-byte diff (flat 18 entries): - All 18 entries: byte-match=True (incl. all NaN sentinel slots) -``` +- **Reviewer:** Pending; the existing record requests Joey Kleingers or another OrientationAnalysis engineer. +- **Date:** Pending. -This is the canonical "100% certainty" proof that: -1. The only behavioral deviations between legacy and SIMPLNX for this filter are the four documented (D1, D2, D4, D6 — D5 retracted to UX-only). -2. After applying all four fixes to the local build of the legacy source (the D1 divisor fix and the D4+D6 Eigen/double/Hex_Low fix), the patched legacy build produces output identical to SIMPLNX at the bit level. -3. No other latent algorithmic difference exists in this filter that the V&V cycle missed. +The requested review covers the tilt derivation, the F2/F5/F6 averages, and the neighbor lists derived from the cell layout. ## Regenerated to fix a circular-oracle situation? -**Yes.** The retired exemplar archive (`compute_feature_neighbor_caxis_misalignments.tar.gz`) was a hex-phase-only dataset. The per-mismatch decrement branch in the algorithm never fired on this dataset, so the exemplar values were correct under both the buggy and the fixed code — the exemplar could not have caught the D1 bug. This is a degenerate form of the "circular oracle" pattern from the V&V policy: the exemplar happens to land in a region of the parameter space where the bug is dormant. +No replacement archive was generated. PR #1631 removed the archive comparison test and added independent Class 1 and Class 4 fixtures. The registered archive name and SHA512 remain unchanged from PR #1467. -The inlined Class 1 + Class 4 fixtures replace the not-bug-catching exemplar with derived-truth oracles plus a deliberately-bug-exposing realistic microstructure that exercises 3 distinct per-feature configurations of the bug. The retired archive was unique to this filter (no other filter test consumed it), so its `download_test_data` line in `test/CMakeLists.txt` was removed entirely. +The old consumer used stored expected arrays from a hexagonal dataset. That coverage could not expose exclusions in a mixed-structure list. This is a coverage limitation, not proof that the archive had a circular generation process. The current fixtures include the excluded neighbors needed to detect D1. diff --git a/src/Plugins/OrientationAnalysis/vv/provenance/ComputeFeatureNeighborMisorientationsFilter.md b/src/Plugins/OrientationAnalysis/vv/provenance/ComputeFeatureNeighborMisorientationsFilter.md index 948dbe03ce..540a1643d8 100644 --- a/src/Plugins/OrientationAnalysis/vv/provenance/ComputeFeatureNeighborMisorientationsFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/provenance/ComputeFeatureNeighborMisorientationsFilter.md @@ -1,130 +1,81 @@ -# Exemplar Archive Provenance: Inlined in Test +# Exemplar Archive Provenance: 6_6_stats_test_v2.tar.gz -This sidecar records how the test data used by `ComputeFeatureNeighborMisorientationsFilter`'s Class 1 and Class 4 unit tests was generated. It is the answer to "where did this hand-built data come from?" - -The test data is **inlined** in the test source — there is no separate tar.gz archive, no `download_test_data()` entry for the new fixtures, and no `.dream3d` exemplar file to fetch. (The shared archive `6_6_stats_test_v2.tar.gz` is still referenced in `src/Plugins/OrientationAnalysis/test/CMakeLists.txt` for the moment because `ComputeKernelAvgMisorientationsFilter` (F#5 in the EbsdLib-precision-cycle list) still consumes it. The archive will be removed from `CMakeLists.txt` after the F#5 V&V cycle retires its exemplar test.) +This archive is retired for `ComputeFeatureNeighborMisorientationsFilter` tests. Current tests construct their inputs and independent expectations in C++. The archive remains registered in `src/Plugins/OrientationAnalysis/test/CMakeLists.txt` and supplies source data for the archived Small IN100 comparison. --- ## Archive identity -| Field | Value | -|-------------------|----------------------------------------------------------------------------------------------------------------------------------------| -| **Archive** | Inlined (no separate archive) | -| **SHA512** | N/A | -| **Used by tests** | `OrientationAnalysis::ComputeFeatureNeighborMisorientationsFilter: Class 1 - Single Phase Two Neighbors` | -| | `OrientationAnalysis::ComputeFeatureNeighborMisorientationsFilter: Class 1 - Mixed Phase Neighbors (exposes divisor bug)` | -| | `OrientationAnalysis::ComputeFeatureNeighborMisorientationsFilter: Class 1 - Mismatch Last Order` | -| | `OrientationAnalysis::ComputeFeatureNeighborMisorientationsFilter: Class 4 - Invariants` | -| **Generated by** | Claude (Opus 4.7, Anthropic) under direction of Michael Jackson | -| **Generated on** | 2026-06-02 | - -## How it was generated - -The dataset is a hand-rolled in-memory `DataStructure` designed as a **Class 1 (Analytical) oracle** with a paired **Class 4 (Invariant)** check. It systematically covers the three branches of the per-feature outer loop / per-neighbor inner loop in `ComputeFeatureNeighborMisorientations::operator()()`: - -1. **Same-phase same-Laue-class neighbor pair** (the happy path — write a numeric misorientation into the list, add to the avg accumulator). -2. **Different-phase neighbor pair** (the phase-mismatch branch — write `NaN` into the list, decrement the divisor). -3. **A mix of branches 1 and 2 inside the same feature's neighbor list** (the bug-exposing path — the per-feature average must use the count of phase-matched neighbors, not the full neighbor-list length, and the divisor must be preserved across j-iterations). - -### Scaffold structure - -A single `ImageGeom` named `ImageGeometry` with 1 voxel (dims = 1×1×1) holds a `CellFeatureData` `AttributeMatrix` with `numTuples = N+1` (N = number of features in the fixture; the +1 is the index-0 background-feature slot that the algorithm skips). Inside `CellFeatureData`: - -- `Phases` (`Int32Array`) — per-feature phase index -- `AvgQuats` (`Float32Array`, 4 components per tuple) — per-feature average quaternion -- `NeighborList` (`NeighborList`) — per-feature list of neighbor feature indices -- `MisorientationList` (`NeighborList`) — output, allocated by the filter's `OutputActions` -- `AvgMisorientations` (`Float32Array`, optional) — output, allocated when `ComputeAvgMisors=true` - -A separate `CellEnsembleData` `AttributeMatrix` with `numTuples = P+1` (P = number of phases in the fixture; the +1 is the index-0 sentinel slot) holds: - -- `CrystalStructures` (`UInt32Array`) — per-phase Laue-class index (`0` = Hex_High, `1` = Cubic_High, etc., per EbsdLib `LaueOps::GetAllOrientationOps()` ordering) +| Field | Value | +|---|---| +| **Archive** | `6_6_stats_test_v2.tar.gz` — retired for this filter's tests | +| **SHA512** | `e84999dec914d81efce4fc4237c49c9bf32e48381b1e79f58aa4df934f0d7606cd7a948f9a5e7b17a126a7944cc531b531cfdc70756ca3e2207b20734e089723` | +| **Used by tests** | None of this filter's current tests. PR #1631 removed the archive consumer `OrientationAnalysis::ComputeFeatureNeighborMisorientationsFilter`. | +| **Generated by** | Jared Duffey (inferred from PR #1124) | +| **Generated on** | — | +| **Generated at commit** | `dc4544512` (PR #1124 merge, inferred) | -The scaffold helpers are in `ComputeFeatureNeighborMisorientationsTest.cpp` namespace `AnalyticalFixtures`: +The first registration of this archive name is PR #1124. Its OrientationAnalysis CMake change supplies the same SHA512 as the current registration. The four inline oracle fixtures entered the test source in PR #1631 (`ec22ac375`). The hidden chunk-boundary fixture entered in PR #1750 (`8914ca68f`). -- `CreateScaffold(numFeatures, numPhases)` — builds the geometry and AttributeMatrices. -- `BuildArgs(dataStructure, computeAvgMisors)` — wires the standard input/output paths into an `Arguments` object. -- `SetAvgQuat(dataStructure, featureIdx, quatVec)` — writes a quaternion into the `AvgQuats` array. -- `QuatFromPhi1Deg(phi1Deg)` — produces an `ebsdlib::QuatD` for a pure φ1 Bunge ZXZ rotation `(φ1, 0, 0)` about the z-axis. - -### Orientation convention - -All Class 1 fixtures use **pure φ1 Bunge ZXZ Euler rotations** `(φ1, 0, 0)` with `Φ = φ2 = 0`. These collapse to a single rotation about the z-axis. For cubic Laue class (`Cubic_High`), the symmetry group includes a 4-fold proper rotation about each crystallographic axis — but cubic's 4-fold-about-c reduces the φ1 difference modulo 90°. By keeping φ1 differences ≤ 45°, the symmetry-reduced minimum-rotation magnitude is exactly `|Δφ1|`. This is what makes the oracle closed-form: no numerical sym-op search required, the expected misorientation is `|φ1_neighbor - φ1_focal|` in degrees. - -### Fixture-by-fixture derivation - -#### Fixture 1 — `Class 1 - Single Phase Two Neighbors` - -- 3 features, 1 phase (Cubic_High). -- Feature 1 (focal): `(0°, 0°, 0°)`. Feature 2: `(5°, 0°, 0°)`. Feature 3: `(10°, 0°, 0°)`. -- `NeighborList[1] = [2, 3]` (focal feature has two neighbors). -- Expected per-neighbor misorientations: `5°` and `10°`. -- Expected per-feature average: `(5 + 10) / 2 = 7.5°`. - -This is the basic-path test. All neighbors are same-phase, so no decrements; divisor = list length = 2 = number of matches. +## How it was generated -#### Fixture 2 — `Class 1 - Mixed Phase Neighbors (exposes divisor bug)` +The retired regression test read `6_6_stats_test_v2.dream3d` as Small IN100 data and compared a calculated neighbor list with its stored `MisorientationList`. PR #1631 replaced that test with hand-constructed data in `src/Plugins/OrientationAnalysis/test/ComputeFeatureNeighborMisorientationsTest.cpp`. `AnalyticalFixtures::CreateScaffold()` creates the geometry, feature arrays, ensemble arrays, and empty neighbor lists. `SetAvgQuat()` and `QuatFromPhi1Deg()` supply rotations about Z as float32 quaternions in `(x,y,z,w)` order. -- 4 features, 2 phases (both Cubic_High to keep the math simple; the bug is triggered by *phase index mismatch*, not by Laue-class mismatch). -- Feature 1 (focal): phase 1, `(0°, 0°, 0°)`. -- Feature 2: phase 1, `(5°, 0°, 0°)` (match). -- Feature 3: phase 2, `(15°, 0°, 0°)` (mismatch). -- Feature 4: phase 1, `(10°, 0°, 0°)` (match). -- `NeighborList[1] = [2, 3, 4]`. Note the order: `[match, mismatch, match]` — the last neighbor is a match. -- Expected per-neighbor misorientations: `5°`, `NaN`, `10°`. -- Expected per-feature average: `(5 + 10) / 2 = 7.5°` (divisor = 2 phase-matched neighbors). +## Canonical oracle output -This is the bug-exposing path. The pre-fix divisor calculation reassigns `tempMisoList = featureNeighborList.size()` (= 3) on every j-iteration, so the per-mismatch decrement done at j=1 is clobbered by the assignment at j=2. The pre-fix code produces `(5 + 10) / 3 = 5.0°`. The post-fix code produces the correct `7.5°`. +The correctness oracle is in the C++ test source. Stored archive outputs and legacy outputs do not define its expected values. -A fixture where the last neighbor is a mismatch (rather than a match) would NOT expose the bug, because the j=last assignment would be followed by the decrement, leaving divisor = N - 1 = 2 (which happens to match the correct value by accident for this neighbor count). That accidental-correct scenario is captured by Fixture 3 below as a control. +The scaffold has a 1×1×1 image with unit spacing and zero origin. The algorithm reads feature data rather than cell coordinates, so image dimensions and origin do not affect this oracle. `numFeatures` includes background feature zero. Outputs are named `MisorientationListOut` and `AvgMisorientationsOut` under `ImageGeometry/FeatureData`. -#### Fixture 3 — `Class 1 - Mismatch Last Order` +The Class 1 fixtures use cubic rotations of 0°, 5°, and 10°. These differences are below the 45° cubic fold, so the expected angles are 5° and 10°. Phase 1 has crystal structure 1 (`Cubic_High`). In mixed-phase fixtures, phase 2 has crystal structure 0 (`Hex_High`), so its neighbors are excluded by crystal-structure mismatch. -- Same scaffold as Fixture 2: 4 features, 2 phases, all rotations. -- `NeighborList[1] = [2, 4, 3]` — order is `[match, match, mismatch]`, last neighbor is a mismatch. -- Expected per-neighbor misorientations: `5°`, `10°`, `NaN`. -- Expected per-feature average: `(5 + 10) / 2 = 7.5°`. +| Fixture | Input at focal feature 1 | Expected output | +|---|---|---| +| `Class 1 - Single Phase Two Neighbors` | Four tuples including background; neighbors `[2, 3]`, at 5° and 10° | List `[5°, 10°]`; mean 7.5° | +| `Class 1 - Mixed Phase Neighbors (exposes divisor bug)` | Five tuples; neighbors `[2, 4, 3]`; feature 4 has the incompatible hexagonal structure | List `[5°, NaN, 10°]`; mean 7.5° | +| `Class 1 - Mismatch Last Order` | Same data with neighbors `[2, 3, 4]` | List `[5°, 10°, NaN]`; mean 7.5° | +| `Class 4 - Invariants` | Five tuples; neighbors `[4, 2, 3]`; compatible rotations 7.5° and 12°, with feature 4 incompatible | Three entries; finite angles in `[0°, 62.8°]`; mean equals their sum divided by their count | -This is the control case. Both pre-fix and post-fix code produce `7.5°` here, because the pre-fix code's last-iteration assignment-then-decrement leaves divisor = 3 - 1 = 2 by accident. Without Fixture 2 to expose the bug, the suite would have shipped a divisor bug that happened to pass tests on this neighbor ordering. +Feature 4 has a 99° input rotation in the mixed fixtures. The angle is not calculated for its incompatible pair. The invariant test checks only feature 1 and requires a positive finite-entry count; it does not assert an all-incompatible or empty-list average. -The presence of Fixture 3 alongside Fixture 2 makes the test suite robust to the bug pattern under both common neighbor orderings. +The hidden `genuine HDF5 1MiB-chunk tail oracle` constructs 65,537 feature tuples. It uses 65,536 quaternion tuples per physical chunk, then a one-tuple tail. Features 65,535 and 65,536 have rotations of 15° and 30° and each names feature 1, at 0°, as its only neighbor. The test asserts each angle and average against those literals. -#### Fixture 4 — `Class 4 - Invariants` +**Legacy comparison artifacts.** Run against DREAM.3D v6.5.171 on 2026-09-17: 0 of 5 complete output cases match at `rtol=1e-6, atol=1e-6`. All five have numerical differences; the middle-mismatch and mixed-phase Small IN100 cases also expose D1. A local proof version matches all 5 cases within that tolerance, with paired NaNs. The first three proof comparisons have exact numeric equality. -- Same data as Fixture 1, but exercises Class 4 invariants rather than a specific expected value. -- Invariants asserted: - 1. **Non-negativity:** Every entry in the per-neighbor `MisorientationList` is either `NaN` (phase mismatch) or ≥ `0°`. Misorientation magnitude cannot be negative. - 2. **Upper bound:** Every non-NaN entry in `MisorientationList` is ≤ `62.8°` (the maximum possible cubic-to-cubic misorientation as derived in Mackenzie 1958 / Morawiec). - 3. **Averaging formula:** For each focal feature `i`, `AvgMisorientations[i] = (sum of non-NaN entries in MisorientationList[i]) / (count of non-NaN entries in MisorientationList[i])`. This is the load-bearing invariant for the divisor bug — Fixture 2's bug-exposing path fails this invariant on pre-fix code as well as failing the specific-value `REQUIRE` in Fixture 2. +`vv_work:ComputeFeatureNeighborMisorientationsFilter/ReadMe.md` identifies the stock application runner and its `1.2.828.f45085c83` version banner. `vv_work:ComputeFeatureNeighborMisorientationsFilter/comparison/summary.md` and `vv_work:ComputeFeatureNeighborMisorientationsFilter/comparison/results.json` record the array comparisons and divisor checks. Input identity checks pass for all five shared inputs. -The Class 4 invariants are oracle-agnostic — they hold for any input regardless of orientation values. This is what makes them a robust complement to the Class 1 fixtures: they would catch regression of the divisor bug even if the specific orientation values in the Class 1 fixtures were changed. +| Case | DREAM.3D v6.5.171 versus SIMPLNX | Local proof version versus SIMPLNX | +|---|---|---| +| Single phase, two neighbors | Maximum neighbor-angle difference `3.05176e-5°`; mean difference `2.95639e-5°` | Exact numeric equality | +| Incompatible neighbor in the middle | Legacy mean `4.9999804°`; SIMPLNX mean `7.5°` | Exact numeric equality | +| Incompatible neighbor last | Maximum neighbor-angle difference `3.05176e-5°`; mean difference `2.95639e-5°` | Exact numeric equality | +| Cubic Small IN100 | 5,833 of 6,312 neighbor entries differ; maximum `0.000337124°`. 547 of 620 means differ. | Maximum angle or mean difference `7.62939453125e-6°` | +| Mixed-phase Small IN100 | 4,036 of 6,312 neighbor entries and 608 of 620 means differ; 37 mean NaN patterns differ. | Maximum angle or mean difference `7.62939453125e-6°`; all NaN patterns match | -### Conversion path in the test +The analytical inputs come from `vv_work:ComputeFeatureNeighborMisorientationsFilter/scripts/make_inputs.py`. The production cases derive from the Small IN100 archive; the mixed case assigns hexagonal symmetry to each positive feature divisible by five. The NX pipelines are under `vv_work:ComputeFeatureNeighborMisorientationsFilter/pipelines/nx/`. The comparison report is `vv_work:ComputeFeatureNeighborMisorientationsFilter/comparison/Legacy_Comparison_Report.md`. -Unlike `ComputeFeatureFaceMisorientationFilter`, this filter consumes `AvgQuats` directly — no `ChangeAngleRepresentationFilter` / `ConvertOrientationsFilter` step. The test fixtures write quaternions directly into `AvgQuats` via the helper `SetAvgQuat(dataStructure, featureIdx, QuatFromPhi1Deg(angleInDegrees))`. The `QuatFromPhi1Deg` helper internally produces `ebsdlib::QuatD(0, 0, sin(phi1/2), cos(phi1/2))` (the standard z-axis pure rotation in the quaternion convention used by EbsdLib). +The comparison predates algorithm commit `4ec21e93f`, dated 2026-09-20, from PR #1568. That commit adds phase-index checks, changes the Laue-index bound expression, and renames local variables in `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeFeatureNeighborMisorientations.cpp`. The report therefore does not use the September comparison as a result for the current algorithm. -## Canonical oracle output +| DataPath | Source of expected values | +|---|---| +| `/ImageGeometry/FeatureData/MisorientationListOut` | Class 1: 5° and 10° cubic rotation differences, with NaN for the incompatible pair; hidden boundary fixture uses 15° and 30°. | +| `/ImageGeometry/FeatureData/AvgMisorientationsOut` | Class 1: `(5 + 10) / 2 = 7.5°`; a single compatible neighbor gives its angle. Class 4: sum of finite list entries divided by their count. | -| DataPath | Source of expected values | -|-------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------| -| `/ImageGeometry/CellFeatureData/MisorientationList` | Class 1 analytical (closed-form, `|Δφ1|` for pure φ1 rotations under cubic symmetry with `Δφ1 ≤ 45°`; `NaN` on phase mismatch). | -| `/ImageGeometry/CellFeatureData/AvgMisorientations` | Class 1 analytical (arithmetic mean of non-NaN entries from above) + Class 4 invariant (non-negativity, upper bound, formula). | +The three ordinary analytical fixtures compare angles and means within `1e-3°`. The invariant fixture compares the mean within `1e-4°` and checks finite angles from 0° through 62.8°. All pass in CI at `419f9db35`. -The expected values are hard-coded into the test as `REQUIRE(Approx(misorientationListEntry).margin(1e-3))` and `REQUIRE(Approx(avgMisorientations[i]).margin(1e-3))` checks. Tolerances are set to `1e-3°` — comfortably wider than the EbsdLib 2.4.1 precision improvement's sub-`0.0001°` shift, but tight enough to catch the divisor bug's `2.5°` magnitude error. +**Test sensitivity verification.** Moving divisor initialization inside the neighbor loop changes the middle-mismatch result from 7.5° to 5°. The mismatch-last control remains 7.5° for its one excluded neighbor. The separate invariant fixture uses a mismatch-first order and verifies the mean relation with 7.5° and 12° input rotations. ## Oracle provenance (Classes 2, 3, 5 only) -N/A — Class 1 and Class 4 oracles only. No reference-library invocation, no paper-figure reproduction, no expert-visual sign-off needed. +Not applicable. The unit-test oracles are Classes 1 and 4. The independent comparison scripts and the local proof version explain migration differences; they do not supply the C++ expected values. ## Second-engineer oracle review -- **Reviewer:** *Pending — recommend Joey Kleingers or another OA-domain engineer review the divisor-bug analysis, specifically the claim that the bug fires only when the last-iterated neighbor is a phase match, and confirm the `7.5°` expected value derivation for Fixtures 2 and 3.* -- **Date:** *YYYY-MM-DD (pending)* -- **Skip reason** (if skipped): *N/A — second-engineer review is recommended; not yet performed.* +- **Reviewer:** Pending — no second engineer is named in the report sign-off. +- **Date:** Pending. ## Regenerated to fix a circular-oracle situation? -N/A — the inlined hand-built fixture is brand-new for this V&V cycle. The pre-V&V test (now retired) consumed the shared `6_6_stats_test_v2.tar.gz` archive as a regression-against-reference exemplar. That archive was a circular oracle (its `AvgMisorientations` values were produced by the same pre-fix algorithm with the same divisor bug, so the exemplar would only have validated reproduction of the buggy output). The retired test additionally had an `[.][UNIMPLEMENTED][!mayfail]` stub for the `ComputeAvgMisors=true` path that prevented any CI coverage of the buggy code path. +PR #1631 replaces this filter's archive regression with independent inline fixtures. Its test-source change records that the archive outputs had been regenerated from SIMPLNX. The archive was not regenerated for the replacement fixtures. -The inlined Class 1 + Class 4 fixtures replace the circular-oracle exemplar with derived-truth oracles. The shared archive remains referenced in `CMakeLists.txt` only for `ComputeKernelAvgMisorientationsFilter` (F#5 in the EbsdLib-precision V&V cycle); it will be removed entirely once F#5's V&V cycle retires its exemplar consumption. +The same PR removes the empty `Misorientation Per Feature` test with `[.][UNIMPLEMENTED][!mayfail]` tags. That stub had no assertions. The replacement fixtures enable averaging and assert its output, including the divisor-sensitive ordering. The archive remains registered for other consumers; this filter's current tests do not load it. diff --git a/src/Plugins/OrientationAnalysis/vv/provenance/ComputeFeatureReferenceCAxisMisorientationsFilter.md b/src/Plugins/OrientationAnalysis/vv/provenance/ComputeFeatureReferenceCAxisMisorientationsFilter.md index a5c5b9700e..b3fb8ab445 100644 --- a/src/Plugins/OrientationAnalysis/vv/provenance/ComputeFeatureReferenceCAxisMisorientationsFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/provenance/ComputeFeatureReferenceCAxisMisorientationsFilter.md @@ -1,180 +1,108 @@ -# Exemplar Archive Provenance: Inlined in Test +# Exemplar Archive Provenance: caxis_data.tar.gz -This sidecar records how the test data used by `ComputeFeatureReferenceCAxisMisorientationsFilter`'s Class 1 and Class 4 unit tests was generated. It is the answer to "where did this hand-built data come from?" - -The test data is **inlined** in the test source — there is no separate tar.gz archive for the V&V fixtures, no `download_test_data()` entry tied to this filter's V&V cycle, and no `.dream3d` exemplar file to fetch for the Class 1 / Class 4 tests. - ---- +This sidecar records the archive and the analytical fixtures for `ComputeFeatureReferenceCAxisMisorientationsFilter`. The archive supplies input for the invalid-execution test. The test source supplies the independent expected outputs. ## Archive identity -| Field | Value | -|-------------------|------------------------------------------------------------------------------------------------------------------------------------------------| -| **Archive** | Inlined (no separate archive) | -| **SHA512** | N/A | -| **Used by tests** | `OrientationAnalysis::ComputeFeatureReferenceCAxisMisorientationsFilter: Class 1 — Simple Hex Triple` | -| | `OrientationAnalysis::ComputeFeatureReferenceCAxisMisorientationsFilter: Class 1 — Realistic Microstructure (exposes divide-by-zero)` | -| | `OrientationAnalysis::ComputeFeatureReferenceCAxisMisorientationsFilter: Class 1 — All-Identical Orientation Feature` | -| | `OrientationAnalysis::ComputeFeatureReferenceCAxisMisorientationsFilter: Class 4 — Invariants` (3 SECTIONs) | -| **Generated by** | Claude (Opus 4.7, Anthropic) under direction of Michael Jackson | -| **Generated on** | 2026-06-10 | - ---- - -## Retired test (consumed an exemplar archive — now superseded) - -| Field | Value | -|-------------------|------------------------------------------------------------------------------------| -| **Test name** | `Valid Filter Execution` (in `test/ComputeFeatureReferenceCAxisMisorientationsTest.cpp`) | -| **Archive** | `caxis_data.tar.gz` | -| **SHA512** | `56468d3f248661c0d739d9acd5a1554abc700bf136586f698a313804536916850b731603d42a0b93aae47faf2f7ee49d4181b1c3e833f054df6f5c70b5e041dc` | -| **Contents used** | `7_0_find_caxis_data.dream3d` — exemplar arrays `FeatureReferenceCAxisMisorientations`, `FeatureAvgCAxisMisorientations`, `FeatureStdevCAxisMisorientations` were compared against the SIMPLNX-generated outputs (`NX_*` counterparts) with `UnitTest::CompareFloatArraysWithNans` at `UnitTest::EPSILON` tolerance. | -| **Retired on** | 2026-06-10 | -| **Retired by** | This V&V cycle | -| **Reason** | Circular oracle. The exemplar arrays were produced by a prior SIMPLNX run (or a pre-EbsdLib-2.4.1 SIMPLNX run); they cannot serve as an independent oracle per V&V policy line 33 ("Legacy 6.5.171 produced this output" is never a valid oracle for correctness — the equivalent here is "SIMPLNX-then produced this output"). Any algorithmic drift between SIMPLNX-then and SIMPLNX-now would silently pass when the exemplar was regenerated against SIMPLNX-now's output. | -| **CMakeLists.txt**| `download_test_data(... caxis_data.tar.gz ...)` line in `src/Plugins/OrientationAnalysis/test/CMakeLists.txt` **RETAINED** — the archive is also consumed by `ComputeCAxisLocationsTest.cpp` (lines 27, 61), which is unrelated to this V&V cycle. Only this filter's exemplar consumer is retired; the archive download is shared. | - -The two surviving TEST_CASEs in `test/ComputeFeatureReferenceCAxisMisorientationsTest.cpp` (`InValid Filter Execution` and `SIMPL Backwards Compatibility`) continue to load `caxis_data.tar.gz` — but as a starting `DataStructure` for the error-path test, NOT as an oracle for output comparison. Those tests are unchanged from pre-V&V and exercise paths 1 (preflight error) and the SIMPL 6.4/6.5 conversion path respectively. - ---- - -## How the inlined fixtures were generated - -The dataset is a hand-rolled in-memory `DataStructure` designed as a **Class 1 (Analytical) oracle** with a paired **Class 4 (Invariant)** check. It systematically covers the 8 algorithmic paths in `ComputeFeatureReferenceCAxisMisorientations::operator()()`: +| Field | Value | +|---|---| +| **Archive** | `caxis_data.tar.gz` | +| **SHA512** | `56468d3f248661c0d739d9acd5a1554abc700bf136586f698a313804536916850b731603d42a0b93aae47faf2f7ee49d4181b1c3e833f054df6f5c70b5e041dc` | +| **Used by tests** | `OrientationAnalysis::ComputeFeatureReferenceCAxisMisorientationsFilter: InValid Filter Execution` | +| **Generated by** | Jared Duffey (inferred from PR #1004) | +| **Generated on** | 2024-06-25 to 2024-07-09 (inferred from PR #1004) | +| **Generated at commit** | `31406a49a` (PR #1004 merge, inferred) | -1. All-non-hex preflight early-exit → error `-9802`. **Exercised by** the surviving `InValid Filter Execution` test (mutates `CrystalStructures[1] = Cubic_High` in the `caxis_data` starting state). -2. Mixed-phase warning `-9803` emitted to `Result<>::warnings()`. **Exercised by** Fixture 2 (Realistic Microstructure). -3. Per-cell normal branch (hex + valid feature + valid phase) → compute `arccos(c1 · avgCAxis)` folded to `[0°, 90°]`. **Exercised by** Fixtures 1, 2, 3. -4. Per-cell skip branch (non-hex OR `featureId == 0` OR `cellPhase == 0`) → write `0.0f`. **Exercised by** Fixture 2 (F3 cubic cells). -5. Per-feature average `sum / counts`. **Exercised by** all 4 fixtures. When `counts == 0` (feature with no hex cells), IEEE 754 `0.0f / 0 = NaN` produces the correct analytical output; exercised by Fixture 2 F3. -6. Per-cell stddev accumulation `Σ (cellRefCAxisMis − featAvg)²`. **Exercised by** all 4 fixtures. NaN from the per-feature average propagates through the squared-difference accumulator for non-hex features. -7. Per-feature stddev finalize `sqrt(sumSqDiff / counts)`. **Exercised by** all 4 fixtures. The all-non-hex case (Fixture 2 F3) yields `sqrt(NaN / 0) = NaN`, which is the analytical expected value. -8. Cancellation `m_ShouldCancel` — structurally covered at the four documented cancel-check sites (lines 105, 169, 184, 197). Not exercised by the V&V fixtures by design; requires cancel-signal injection. +PR #1004 first registers the exact name `caxis_data.tar.gz` with the current SHA512 in `src/Plugins/OrientationAnalysis/test/CMakeLists.txt`. Earlier registrations use `6_6_caxis_data.tar.gz` or `6_6_caxis_filter_data.tar.gz` with different hashes. The current archive's calculated SHA512 matches the registered value. -### Scaffold structure +The archive contains the following files under `caxis_data/`: -The `AnalyticalFixtures` namespace (anonymous-wrapped to keep symbols TU-local) at the top of `ComputeFeatureReferenceCAxisMisorientationsTest.cpp` provides: +| File | Content | +|---|---| +| `6_6_caxis_segment_features.dream3d` and `.xdmf` | Shared c-axis segmentation data | +| `7_0_find_caxis_data.dream3d` and `.xdmf` | Cell and feature inputs, and stored filter outputs | +| `BRS111d-redo_461.ang` | EBSD input file | +| `Combo-EBSD-120130.osc_r0c0.ang` | EBSD input named by the embedded generation pipeline | -- `CreateScaffold(nX, nY, nZ, numFeatures, numCrystalStructures)` — constructs the in-memory `DataStructure` with an ImageGeom, Cell/Feature/Ensemble AMs, and pre-allocated input arrays. Defaults: every cell assigned to feature 1 / phase 1 / identity quat; every feature `AvgCAxes = (0, 0, 1)` (sample-z); ensemble sentinel at index 0 = 999. -- `QuatFromPhiDeg(phiDeg)` — returns the quaternion for a Bunge ZXZ `(0, Φ, 0)` Euler rotation: `{sin(Φ/2), 0, 0, cos(Φ/2)}`. This is a pure x-rotation; the crystal c-axis (originally along z) tilts to `[0, sin(Φ), cos(Φ)]` in the sample frame. -- `CAxisFromPhiDeg(phiDeg)` — returns the c-axis vector for a feature whose average orientation is a pure-Φ tilt: `[0, sin(Φ rad), cos(Φ rad)]`. This is exactly what the filter expects from upstream `ComputeAvgCAxes`. -- `SetCellQuat(td, cellIdx, q)` / `SetAvgCAxis(td, featureIdx, c)` — write helpers. -- `BuildArgs()` — constructs the `Arguments` object with the standard 9 input/output paths. -- `BuildRealisticMicrostructure()` — constructs the 5×5×1 6-feature scaffold used by Fixture 2 and the Class 4 Invariants test. +## How it was generated -### Orientation convention +`caxis_data/7_0_find_caxis_data.dream3d:/Pipeline/Pipeline` records a generation pipeline from a local proof version. It imports `Combo-EBSD-120130.osc_r0c0.ang`, rotates the reference frames, creates a confidence mask and converts Euler angles to quaternions. It segments features at a 5° c-axis tolerance with feature-ID randomization disabled, replaces FeatureId 1 with 0, then calculates average c-axes and feature-reference c-axis angles. Step 08 runs `FindFeatureReferenceCAxisMisorientations`, and step 15 writes the data file. -All Class 1 fixtures use **pure Bunge ZXZ Euler rotations `(0, Φ, 0)`** — rotations about the x-axis by Φ degrees. Closed-form derivation: +The input and stored output arrays are under `/DataContainers/DataContainer/CellData`, `/DataContainers/DataContainer/CellFeatureData`, and `/DataContainers/DataContainer/CellEnsembleData`. `InValid Filter Execution` loads that data, changes `CrystalStructures[1]` to Cubic-High and checks execution failure. `SIMPL Backwards Compatibility` reads only the JSON fixtures in `test/simpl_conversion/6_4/` and `test/simpl_conversion/6_5/`; it does not load this archive. -The orientation matrix for a pure-Φ rotation about x is: -``` -R(Φ) = [[1, 0, 0], - [0, cos(Φ), -sin(Φ)], - [0, sin(Φ), cos(Φ)]] -``` +The analytical fixtures are constructed by `AnalyticalFixtures::CreateScaffold()` in `src/Plugins/OrientationAnalysis/test/ComputeFeatureReferenceCAxisMisorientationsTest.cpp`. The helper creates an Image Geometry, cell, feature and ensemble Attribute Matrices, and the input arrays. It assigns cells to feature 1 and phase 1 with identity quaternions, sets feature reference c-axes to sample Z, and puts the unknown-structure sentinel at ensemble index 0. Each fixture then changes the inputs it needs. -The algorithm computes `c1 = R^T · [0, 0, 1] = [0, sin(Φ), cos(Φ)]` — the c-axis vector tilted from the global z-axis by Φ degrees. The feature's pre-computed `AvgCAxes` input is set to the same form: `[0, sin(Φ_avg), cos(Φ_avg)]`. +**Orientation construction** -The per-cell misorientation reduces to: -- `cos(miso) = c_cell · c_avg = sin(Φ_cell)·sin(Φ_avg) + cos(Φ_cell)·cos(Φ_avg) = cos(Φ_cell − Φ_avg)` -- `arccos(cos(Φ_cell − Φ_avg)) = |Φ_cell − Φ_avg|` -- Folded to `[0°, 90°]` via `if(w > 90°) w = 180° − w`. No-op for all fixtures (max ΔΦ used is 5°). +`QuatFromPhiDeg(Φ)` supplies `[sin(Φ/2), 0, 0, cos(Φ/2)]`, with degree-to-radian conversion. `CAxisFromPhiDeg(Φ)` supplies `[0, sin(Φ), cos(Φ)]`. The dot product of the cell and feature c-axes is `cos(Φ_cell − Φ_avg)`. The expected angle is therefore `abs(Φ_cell − Φ_avg)` folded into [0°, 90°]. The visible fixtures use differences from 0° through 5°, so they do not test the fold above 90°. -This makes the per-cell oracle closed-form. The per-feature average + population stddev follow by straight arithmetic on the per-cell values. +`CreateScaffold()` uses zero origin and unit spacing. Physical coordinates do not enter this algorithm. The three visible base fixtures use pure rotations about one axis to make each expected angle independent of an orientation library. These fixtures do not test general rotations about multiple axes. -### Fixture-by-fixture derivation +**Simple Hex Triple** -#### Fixture 1 — `Class 1 — Simple Hex Triple` +The 3×1×1 image contains one Hexagonal-High feature. Cell tilts are 0°, 5° and 10°, and the supplied feature reference tilt is 5°. -- 3×1×1 image, 2 features total (sentinel + 1 hex). -- `(*td.crystalStructures)[1] = ebsdlib::CrystalStructure::Hexagonal_High`. -- Cells: 3 hex cells at Φ = 0°, 5°, 10°. -- `AvgCAxes[F1]` = `CAxisFromPhiDeg(5°)` (the geometric mean of the 3 cell tilts). -- Expected: - - `cellRefCAxisMis = [5°, 0°, 5°]` - - `featAvg[F1] = (5 + 0 + 5) / 3 = 10/3 ≈ 3.3333` - - `featStdev[F1] = sqrt(((5 − 10/3)² + (0 − 10/3)² + (5 − 10/3)²) / 3) = sqrt(50/9) = 5√2/3 ≈ 2.3570` +| Output | Analytical expectation | +|---|---| +| Cell angles | `[5°, 0°, 5°]` | +| Feature mean | `(5 + 0 + 5)/3 = 10/3°` | +| Feature standard deviation | `sqrt(((5 − 10/3)^2 + (0 − 10/3)^2 + (5 − 10/3)^2)/3) = sqrt(50/9)°` | -Basic-path sanity test. Confirms the closed-form `|ΔΦ|` reduction, the per-feature average arithmetic, and the **population** stddev formula (divisor `counts`, not `counts − 1`). +The divisor is the population count, not one less than the count. -#### Fixture 2 — `Class 1 — Realistic Microstructure (exposes divide-by-zero)` +**Realistic Microstructure** -The meaty fixture. 5×5×1 image, 6 features total (sentinel + 5 real). 3 crystal structures: sentinel + `Hexagonal_High` + `Cubic_High`. +The 5×5×1 image has five rows, one feature per row, plus feature-zero storage. F1, F2, F4 and F5 use Hexagonal-High; F3 uses Cubic-High. `BuildRealisticMicrostructure()` supplies the following values. -**Phase assignment:** -- `(*td.crystalStructures)[1] = Hexagonal_High` -- `(*td.crystalStructures)[2] = Cubic_High` -- F1, F2, F4, F5 → phase 1 (hex) -- F3 → phase 2 (cubic, non-hex) +| Feature | Cell tilts (degrees) | Reference tilt | Cell angles (degrees) | Mean | Standard deviation | +|---|---|---|---|---|---| +| F1 | `0, 0, 0, 0, 0` | 0° | `0, 0, 0, 0, 0` | 0° | 0° | +| F2 | `8, 9, 10, 11, 12` | 10° | `2, 1, 0, 1, 2` | 1.2° | `sqrt(0.56)°` | +| F3 | Identity quaternions; cubic phase | 10°, skipped | `0, 0, 0, 0, 0` | NaN | NaN | +| F4 | `20, 20, 20, 20, 20` | 20° | `0, 0, 0, 0, 0` | 0° | 0° | +| F5 | `25, 28, 30, 32, 35` | 30° | `5, 2, 0, 2, 5` | 2.8° | `sqrt(3.76)°` | -**Cell layout** (rows = y, cols = x; each row is 5 cells): +For F2, the angle sum is 6° and the population variance is 0.56 square degrees. For F5, the sum is 14° and the population variance is 3.76 square degrees. The test checks all 25 cell values and all five non-background feature means and standard deviations. -``` -y=0: F1 F1 F1 F1 F1 (all hex, Φ = 0°) -y=1: F2 F2 F2 F2 F2 (hex, Φ = 8°, 9°, 10°, 11°, 12°) -y=2: F3 F3 F3 F3 F3 (cubic — algorithm skip branch) -y=3: F4 F4 F4 F4 F4 (all hex, Φ = 20°) -y=4: F5 F5 F5 F5 F5 (hex, Φ = 25°, 28°, 30°, 32°, 35°) -``` +F3 has no hexagonal contributor. The algorithm explicitly writes quiet NaN for its mean and standard deviation. The test name retains the phrase `exposes divide-by-zero`, but the current implementation does not divide by zero to produce these two outputs. -**Per-feature `AvgCAxes` input** (set via `SetAvgCAxis`): -- F1: `CAxisFromPhiDeg(0°)` -- F2: `CAxisFromPhiDeg(10°)` -- F3: `CAxisFromPhiDeg(10°)` (ignored — cubic phase takes the skip branch) -- F4: `CAxisFromPhiDeg(20°)` -- F5: `CAxisFromPhiDeg(30°)` +**All-Identical Orientation Feature** -**Per-feature expected outputs:** +The 5×1×1 image contains one Hexagonal-High feature. All five cell tilts and the reference tilt are 10°. The test checks five zero cell angles and zero feature mean and standard deviation. -| Feature | Phase | Φ_cells | Misos | counts | Σ misos | featAvg | featStdev | -|---------|-------|-----------------------|--------------------|--------|---------|--------------------|---------------------------------| -| F1 | Hex | 0, 0, 0, 0, 0 | 0, 0, 0, 0, 0 | 5 | 0 | **0.0000°** | **0.0000°** | -| F2 | Hex | 8, 9, 10, 11, 12 | 2, 1, 0, 1, 2 | 5 | 6 | **1.2000°** | `sqrt(0.56) ≈` **0.7483°** | -| F3 | Cubic | (skip branch) | 0, 0, 0, 0, 0 | **0** | 0 | **NaN** | **NaN** | -| F4 | Hex | 20, 20, 20, 20, 20 | 0, 0, 0, 0, 0 | 5 | 0 | **0.0000°** | **0.0000°** | -| F5 | Hex | 25, 28, 30, 32, 35 | 5, 2, 0, 2, 5 | 5 | 14 | **2.8000°** | `sqrt(3.76) ≈` **1.9391°** | +**Class 4 invariants** -**F3 is the load-bearing case.** With 0 hex cells contributing, `counts[F3] = 0`. The algorithm's per-feature finalize executes `featAvgCAxisMis[3] = 0.0f / 0 = NaN` (IEEE 754) at line 176, and the per-feature stddev finalize executes `sqrt(NaN / 0) = NaN` at line 202. The test asserts NaN with `std::isnan(...)` for both `featAvg[3]` and `featStdev[3]`. This confirms that the divide-by-zero behavior at code paths 5 and 7 produces the correct analytical output on the IEEE 754 floating-point environment that SIMPLNX builds on (validated across `NX-Com-Qt69-Vtk95-Rel`). +The invariant case reuses the realistic fixture in three sections. The first checks [0°, 90°] bounds for hexagonal cells and zero for cubic cells. The second independently recomputes each hexagonal feature mean from the cell-angle output. The third checks NaN mean and standard deviation for F3. The Class 1 cases separately assert the population standard deviations. -#### Fixture 3 — `Class 1 — All-Identical Orientation Feature` +**Slice-boundary fixture** -- 5×1×1 image, 2 features total (sentinel + 1 hex). -- All 5 cells at Φ = 10°. -- `AvgCAxes[F1] = CAxisFromPhiDeg(10°)`. -- Expected: `cellRefCAxisMis = [0, 0, 0, 0, 0]`, `featAvg[F1] = 0`, `featStdev[F1] = 0`. +The hidden `genuine HDF5 slice-boundary oracle` case builds a 17×19×4 image. Its 323-cell slices contain three feature-1 contributors at indices 322, 323 and 1291. Their angles are 10°, 20° and 30°, so the expected mean is 20° and standard deviation is `sqrt(200/3)°`. The case also checks zero cell outputs at indices 324 and 646. These assertions address reuse of the slice buffer and accumulation across slices. -Confirms invariant **I5** (all-cells-share-avg-orientation feature → zero everywhere) and acts as a sanity check that the algorithm doesn't introduce numerical drift on a degenerate-but-valid input. - -#### Fixture 4 — `Class 4 — Invariants` (3 SECTIONs, reuses Fixture 2 data) - -Three SECTIONs all using the realistic-microstructure fixture: - -**Sub-section (i) — Range:** every `cellRefCAxisMis[i] ∈ [0°, 90°]` for hex cells (the algorithm's fold `if(w > 90°) w = 180° − w` enforces the upper bound; `arccos` returning non-negative enforces the lower bound). Every `cellRefCAxisMis[i] == 0.0f` for non-hex cells (the skip branch at lines 153-156 writes the exact value). Invariants **I1** + **I2**. - -**Sub-section (ii) — Per-feature averaging formula:** for each hex feature `f`, `featAvg[f]` equals the arithmetic mean of `cellRefCAxisMis[i]` for cells `i ∈ f` with hex+valid phase (recomputed inline from the per-cell array). Invariant **I3**. F3 (cubic) is explicitly skipped in this section because the formula does not apply when `counts == 0` — handled in section (iii). +## Canonical oracle output -**Sub-section (iii) — All-non-hex feature → NaN:** asserts `std::isnan(featAvg[3])` and `std::isnan(featStdev[3])`. Invariant **I6**. **Load-bearing for the V&V finding** that the algorithm produces the correct analytical NaN for features with no contributing hex cells (via IEEE 754 `0.0f / 0 = NaN` propagation through the average + stddev finalize). +The canonical values are defined in C++, not read from the archive. These DataPaths belong to the constructed fixtures. -## Canonical oracle output +| DataPath | Source of expected values | +|---|---| +| `/ImageGeometry/CellData/FeatureReferenceCAxisMisorientations` | Class 1 analytical angle; zero for skipped non-hexagonal cells | +| `/ImageGeometry/CellFeatureData/FeatureAvgCAxisMisorientations` | Class 1 arithmetic mean on the hexagonal fixtures; Class 4 NaN when no cell contributes | +| `/ImageGeometry/CellFeatureData/FeatureStdevCAxisMisorientations` | Class 1 population standard deviation on the hexagonal fixtures; Class 4 NaN when no cell contributes | -| DataPath | Source of expected values | -|-------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `/ImageGeometry/CellData/FeatureReferenceCAxisMisorientations` | Class 1 analytical (closed-form `|Φ_cell − Φ_avg|` for pure-Φ rotations, folded to `[0°, 90°]`; `0.0f` on the skip branch for non-hex / invalid cells). | -| `/ImageGeometry/CellFeatureData/FeatureAvgCAxisMisorientations` | Class 1 analytical (arithmetic mean of per-cell misos for hex+valid cells in feature) + Class 4 invariant (mean-formula, NaN-on-empty). | -| `/ImageGeometry/CellFeatureData/FeatureStdevCAxisMisorientations` | Class 1 analytical (sqrt of population variance with divisor `counts`) + Class 4 invariant (range, NaN-on-empty). | +Finite comparisons use an absolute margin of 0.001°. Empty-feature checks use `std::isnan`. The four visible oracle cases pass in CI at `419f9db35` on Linux, macOS and Windows. -The expected values are hard-coded into each TEST_CASE as `REQUIRE(... == Approx(...).margin(1e-3f))` per-feature checks and `REQUIRE(std::isnan(...))` for the all-non-hex feature. Tolerance set to `1e-3°` — comfortably wider than any expected float32 precision noise from the quaternion→matrix→c-axis pipeline. +The current variance pass adds squared differences for every cell assigned to a feature, while the divisor counts only participating hexagonal cells. The analytical fixtures assign one phase per feature. ## Oracle provenance (Classes 2, 3, 5 only) -N/A — Class 1 and Class 4 oracles only. No reference-library invocation, no paper-figure reproduction, no expert-visual sign-off needed. +Not applicable. Classes 1 and 4 define expected outputs through closed-form formulas and invariants in the test source. The archive's stored outputs and embedded pipeline do not define those expected values. ## Second-engineer oracle review -- **Reviewer:** *Pending — recommend Joey Kleingers or another OA-domain engineer review (a) the closed-form derivation that pure Bunge ZXZ `(0, Φ, 0)` tilts the c-axis by exactly Φ degrees, (b) the Fixture 2 per-feature expected averages (especially F2 and F5 which have non-trivial in-feature spread), and (c) the I6 NaN-propagation expectation for the all-non-hex F3 feature.* -- **Date:** *YYYY-MM-DD (pending)* -- **Skip reason** (if skipped): *N/A — second-engineer review is recommended but not yet performed. The Class 1 derivation reuses the math already reviewed during the sibling `ComputeFeatureNeighborCAxisMisalignmentsFilter` V&V cycle (F#6), which used the same pure-Φ Bunge ZXZ argument with the same `|ΔΦ|` reduction.* +- **Reviewer:** skipped +- **Skip reason:** No second-engineer review is recorded for this filter. The report sign-off names Michael Jackson only. ## Regenerated to fix a circular-oracle situation? -**Yes.** The retired `Valid Filter Execution` test consumed the `caxis_data.tar.gz` archive's `FeatureReferenceCAxisMisorientations` / `FeatureAvgCAxisMisorientations` / `FeatureStdevCAxisMisorientations` exemplar arrays as the oracle for the SIMPLNX-generated outputs. Those exemplar arrays were produced by a prior SIMPLNX run — making the test a textbook circular oracle (SIMPLNX-now compared against SIMPLNX-then). Per V&V policy line 33, this is not a valid correctness check. The inlined Class 1 + Class 4 fixtures replace the not-independent oracle with derived-truth oracles + a load-bearing invariant test for the divide-by-zero path. The archive itself is RETAINED in `test/CMakeLists.txt` because `ComputeCAxisLocationsTest.cpp` still consumes it. +The archive was not regenerated for this V&V. PR #1636 (`c225e8a24`) removed the `Valid Filter Execution` comparison against stored output arrays and added the four analytical and invariant cases. The current archive hash remains the hash registered by PR #1004. + +The embedded pipeline records the legacy-named filter in a local proof version. The archive remains useful as input for the invalid-execution case. Independent correctness for this filter comes from the analytical expectations above. diff --git a/src/Plugins/OrientationAnalysis/vv/provenance/ComputeFeatureReferenceMisorientationsFilter.md b/src/Plugins/OrientationAnalysis/vv/provenance/ComputeFeatureReferenceMisorientationsFilter.md index bc968c63b0..ac5b2eb3a9 100644 --- a/src/Plugins/OrientationAnalysis/vv/provenance/ComputeFeatureReferenceMisorientationsFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/provenance/ComputeFeatureReferenceMisorientationsFilter.md @@ -1,117 +1,92 @@ -# Test Data Provenance: ComputeFeatureReferenceMisorientationsFilter +# Exemplar Archive Provenance: compute_feature_reference_misorientation.tar.gz -This sidecar records how the test data used by `ComputeFeatureReferenceMisorientationsFilter`'s unit tests was generated. - -**Important:** Unlike most simplnx test data sets, **this filter's test data is NOT stored in a `.tar.gz` archive** — it is inline-constructed in C++ within the test source file. The provenance document still applies because the *methodology* (how the data was derived, who derived it, what convention was used) is what needs to be reproducible, not the binary artifact. This sidecar fills the same role as a per-archive provenance sidecar would: it answers "where did this gold-standard data come from?" +The current tests construct inputs and analytical expectations in C++. This record also identifies the retired archive and preserves the source and comparison evidence. --- -## Test data identity - -| Field | Value | -|-----------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------| -| **Data location** | Inline in `src/Plugins/OrientationAnalysis/test/ComputeFeatureReferenceMisorientationsTest.cpp` — `namespace AnalyticalFixtures` | -| **Data format** | C++ source code (no external file, no `download_test_data()` entry, no SHA512) | -| **Used by tests** | All 6 `Class 1` and `Class 4` TEST_CASE blocks (see test file). Pre-existing `_AverageMisorientation` and `_EuclideanDistance` exemplar-tests retired during this V&V cycle. | -| **Generated by** | Michael Jackson (V&V lead) + Claude (analytical derivation + code-generation assist) on `vv/compute_feature_face_misorientations` branch | -| **Generated on** | 2026-06-01 | -| **Generated at commit** | ** | -| **EbsdLib version pin** | 2.4.1 (vcpkg pin; commit `5c8c993` on `/Users/mjackson/Workspace9/EbsdLib`) — recorded for traceability since SIMPLNX's misorientation results are coupled to this pin | - -## How it was generated +## Archive identity -The test data is **hand-derived from first principles** for each of the 6 data fixtures + 1 invariant sweep. The construction methodology is: +| Field | Value | +|---|---| +| **Archive** | `compute_feature_reference_misorientation.tar.gz` — retired; no current test consumes it. | +| **SHA512** | `6ea9c04ca5b0c0439573b5a14bda63592181c6badb4dd325b542fb97ff2a5d492e83d2bac1bf5999612cbdb7697ec48e321549427470f1f23ccd37921c6a95f1` — from `test/CMakeLists.txt` at `f7fec9819`. | +| **Used by tests** | The removed `ComputeFeatureReferenceMisorientationsFilter_AverageMisorientation` and `ComputeFeatureReferenceMisorientationsFilter_EuclideanDistance` tests. Current tests use inline data. | +| **Generated by** | Michael Jackson (inferred from PR #1504) | +| **Generated on** | Pending: archive generation date or PR #1504 open dates | +| **Generated at commit** | `f7fec9819` (PR #1504 merge, inferred) | -1. **Choose a target code path** (or set of paths) that the fixture exercises (see "Code path coverage" in the V&V report). -2. **Pick the smallest possible ImageGeom** that triggers those paths (the 6 fixtures range from 2×2×2 = 8 voxels to 4×3×1 = 12 voxels; one 3×3×2 = 18 voxel fixture verifies the 3D index → coord conversion in `Mode 1`). -3. **Pick quaternions from the Bunge ZXZ Euler subset `(φ1, 0, 0)`** — pure φ1-rotations about the cubic c-axis. Misorientation between two such quaternions reduces in closed form to `|Δφ1|` (modulo cubic 4-fold-about-c-axis symmetry); for Δφ1 ∈ {0°, 5°, 10°} the symmetry reduction is the identity (no fold below the input value), so the expected misorientation is exactly `|Δφ1|`. -4. **For Mode 1 fixtures**, hand-pick `GBEuclideanDistances` values such that the `m_Centers[fid]` selection has a unique closed-form answer (or, for Fixture E, a deterministic tie-break per the `>=` comparison semantics that both legacy and SIMPLNX share). -5. **Derive every expected output value on paper** before writing the test assertion. For the per-voxel `FRM`, this is the symmetry-reduced misorientation. For the per-feature `avgRefMis`, this is `sum / count` over the qualifying voxels (`featureId > 0 && cellPhase > 0`). For the per-feature `EuclideanCenters` (Mode 1 only), this is `imageGeom.getCoordsf(centerVoxelIdx)` evaluated with the fixture's `spacing = 1` and `origin = 0` (yielding cell-center coordinates). -6. **Embed the expected value as a literal** in the test assertion with a tolerance appropriate to the float precision of the output (`1e-3°` for FRM, `1e-5` for centers — these tolerances are tight enough to catch the EbsdLib 2.4.1 precision shift if it ever reverts, and loose enough to absorb harmless rounding). +The plugin's current `src/Plugins/OrientationAnalysis/test/CMakeLists.txt` contains no registration for this archive. PR #1504 first registered its name and the SHA512 above. PR #1629 (`99f3a9865`) removed the consuming tests and the registration. -The construction methodology is reproducible: any engineer can read the test source, see the hand-built `featureIds` / `cellPhases` / `Quats` / `GBEuclideanDistances` / `AvgQuats` arrays, work out the expected misorientation by applying the Bunge ZXZ Euler convention + cubic symmetry rules, and verify the result matches the inline assertion. No external data fetch, no library version drift in the data itself. +## How it was generated -### Per-fixture summary +The retired archive's pipeline reads `T12-MAI-2010/fw-ar-IF1-aptr12-corr.ctf`, crops the EBSD data, segments features and calculates reference misorientations in SIMPLNX. The archive ReadMe records Michael Jackson's manual review for reasonableness on 2026-01-14. Its generating pipeline and ReadMe are at `vv_work:ComputeFeatureReferenceMisorientationsFilter/scratch/compute_feature_reference_misorientation/compute_feature_reference_misorientation.d3dpipeline` and `vv_work:ComputeFeatureReferenceMisorientationsFilter/scratch/compute_feature_reference_misorientation/ReadMe.md`. This archive is separate from the Small IN100 input used in the legacy comparison. -| # | Fixture | Dim | Features | Mode | What it covers | -|---|----------------------------------------|---------|----------|------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| A | `Mode 0 SingleGrainIdentity` | 2×2×2 | 1 | 0 | Basic Mode 0 plumbing; degenerate case where all quats == reference. Expected: all FRM = 0. | -| B | `Mode 0 KnownAngle5deg` | 2×2×2 | 1 | 0 | Mode 0 with a known non-zero misorientation. All quats = 5° about z; AvgQuats[1] = identity. Expected: FRM = 5° everywhere; avg = 5°. | -| C | `Mode 0 MultiGrain EdgeCases` | 4×3×1 | 5 | 0 | Mixed Mode 0 — background voxel (featureId=0), unphased voxels mid-feature, an entire feature with all-unphased voxels (count == 0 → avg = 0 path). | -| D | `Mode 1 KnownCenter` | 3×3×1 | 1 | 1 | Mode 1 with deterministic single-center selection. Center voxel (index 4) has uniquely max `GBEuclideanDistances` and identity quat; perimeter voxels have 5° quat. Tests `m_Centers` selection + `EuclideanCenters` coord writing. | -| E | `Mode 1 MultiGrain CenterIsolation` | 2×3×1 | 2 | 1 | Mode 1 multi-feature with per-feature center isolation. Includes deliberate tied distances within a feature to exercise the `>=` tie-break (later voxel wins). | -| F | `Mode 1 3D Volume` | 3×3×2 | 1 | 1 | Mode 1 on a true 3D volume to verify the `voxelIdx → (x, y, z)` index arithmetic when `dimZ > 1`. Center voxel index 13 = (x=1, y=1, z=1) → cell-center coord (1.5, 1.5, 1.5). | -| G | `Class 4 Invariants Sweep` | varies | varies | both | Re-runs Mode 0 and Mode 1 configurations and asserts the Class 4 invariants only (monotonicity-style properties that hold for *any* input). No value-specific assertions; catches future regressions that preserve specific values. | +Current inputs are built by `AnalyticalFixtures::CreateScaffold()` in `src/Plugins/OrientationAnalysis/test/ComputeFeatureReferenceMisorientationsTest.cpp`. The six analytical fixtures and invariant sweep were added in PR #1629. Pure c-axis rotations encode known misorientation angles; hand-selected grain-boundary distances specify the reference cells. The fixture origin is zero and spacing is one, so a cell index `(x,y,z)` has center `(x+0.5,y+0.5,z+0.5)`. -### Code path coverage +| Fixture | Dimensions | Independent expectation | +|---|---|---| +| `Class 1 - Mode 0 SingleGrainIdentity` | 2×2×2 | Identity orientations give zero angles and means. | +| `Class 1 - Mode 0 KnownAngle5deg` | 2×2×2 | Every cell is 5° from its feature reference; the mean is 5°. | +| `Class 1 - Mode 0 MultiGrain EdgeCases` | 4×3×1 | Cell angles are `[0,5,5,0,0,5,0,0,0,0,0,0]`; feature means are `[0,5,0,5,0]`. | +| `Class 1 - Mode 1 KnownCenter` | 3×3×1 | Center is `(1.5,1.5,0.5)`; its angle is zero and eight other cells have 5° angles. | +| `Class 1 - Mode 1 MultiGrain CenterIsolation` | 2×3×1 | Centers are `(1.5,0.5,0.5)` and `(1.5,2.5,0.5)`; each mean is 10/3°. | +| `Class 1 - Mode 1 3D Volume` | 3×3×2 | Center is `(1.5,1.5,1.5)`; its angle is zero and 17 cells have 5° angles. | +| `Class 4 - Invariants Sweep` | Two 3×3×1 configurations | Angles are nonnegative and at most 62.8° for cubic symmetry; skipped cells and background stay zero; feature means equal participating-cell sums/counts. | +| `genuine HDF5 65536-block tail oracle` | 65537×1×1 | Literal cell angles and means cover both modes; a tied reference at the final cell has center `(65536.5,0.5,0.5)`. | -7 of 8 algorithmic paths covered directly; 1 (cancel-check) implicit. See `ComputeFeatureReferenceMisorientationsFilter.md` §"Code path coverage" for the path table. +The analytical angle tolerance is 0.001°. Center checks use a margin of 0.00001. The feature-mean invariant uses a tolerance of 0.0001°. These fixtures check the filter's reference selection and averaging; their symmetric orientations do not cover every crystal symmetry or a nonzero geometry origin. ## Canonical oracle output -| DataPath | Source of expected values | -|--------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------| -| `/Image Geometry/Cell Data/Cell Reference Misorientations` (after filter runs) | **Class 1 (Analytical)** — hand-derived from pure φ1-rotation Bunge ZXZ Euler convention + cubic 4-fold-about-c-axis symmetry rule. | -| `/Image Geometry/Cell Feature Data/Feature Avg Misorientations` | **Class 1 (Analytical)** — `sum(FRM[v ∈ feature, phase>0]) / count(v ∈ feature, phase>0)` over the same per-voxel FRM values. | -| `/Image Geometry/Cell Feature Data/Feature Euclidean Centers` (Mode 1) | **Class 1 (Analytical)** — `imageGeom.getCoordsf(centerVoxelIdx)` where `centerVoxelIdx` is hand-picked via `GBEuclideanDistances`. | -| (any of the above, in any fixture) | **Class 4 (Invariant)** — predicates documented in the `ClassFourInvariants::AssertInvariants()` helper at top of the test source. | - -## Oracle provenance (Class 2, 3, 5) +The oracle values live in test source. No archived array supplies the current correctness oracle. -### Class 2 — Reference implementation +| DataPath | Source of expected values | +|---|---| +| `/DataContainer/CellData/Cell Reference Misorientations` | Class 1 literal angles from pure c-axis rotations; Class 4 range and skip predicates. | +| `/DataContainer/CellFeatureData/Feature Avg Misorientations` | Class 1 literal means; Class 4 sum/count over participating cells and zero for empty features. | +| `/DataContainer/CellFeatureData/Feature Euclidean Centers` | Class 1 literal coordinates of the selected reference cells. | -Not used. The misorientation math is delegated to `ebsdlib::LaueOps::calculateMisorientation`, which is itself verified against the Rowenhorst 2015 paper-based oracle in EbsdLib's own test suite. We trust that math transitively at the pinned EbsdLib version (2.4.1, commit `5c8c993`). +**Legacy comparison evidence** -### Class 3 — Paper-based +The run against DREAM.3D v6.5.171 on 2026-09-17 used SIMPLNX `8b80721c20853d891d5040f9134315af1d93b2b6` and the stock application PipelineRunner 1.2.828. Three of three direct Mode 0 analytical fixtures agree with their independent expected angles within 0.001°. Only the identity fixture is bit-identical between the two applications. All four direct Mode 1 cases crash in DREAM.3D v6.5.171; all complete in SIMPLNX and in a local proof version with the feature-count correction. -Indirectly applicable. The Bunge ZXZ Euler convention used for the φ1-rotation quaternion construction is documented in: +| Comparison | Result recorded on 2026-09-17 | +|---|---| +| Direct analytical Mode 1, local proof version versus SIMPLNX | Three of three fixtures match exactly for both misorientation arrays. | +| Small IN100 Mode 0, DREAM.3D v6.5.171 versus SIMPLNX | Maximum cell difference 0.07380°; maximum feature difference 0.01761°. | +| Small IN100 Mode 0, local proof version versus SIMPLNX | Maximum cell difference 1.907e-6°; maximum feature difference 1.669e-5°. | +| Small IN100 Mode 1, local proof version versus SIMPLNX | Maximum cell difference 9.537e-7°; maximum feature difference 6.437e-6°. | +| Mode 1 emulation versus direct SIMPLNX Mode 1 | Four of four cases match exactly for both misorientation arrays. | -- Rowenhorst, D., Rollett, A. D., Rohrer, G. S., Groeber, M., Jackson, M. A., Konijnenberg, P. J., & De Graef, M. (2015). *Consistent representations of and conversions between 3D rotations.* Modelling and Simulation in Materials Science and Engineering, 23(8), 083501. DOI: 10.1088/0965-0393/23/8/083501. +The emulation independently selects the last cell with the maximum distance, places its quaternion in AvgQuats, and runs Mode 0. The comparison script reproduces each application's stored feature means with its own accumulation order. Thus, the remaining average differences are consistent with float32 summation order. -This filter does not implement that paper's algorithms; it consumes them via EbsdLib. No paper figure/equation is reproduced inline in this filter's test code (the citation is included here as the reference for the convention used to construct the data fixtures, not as the oracle for the filter's algorithm). +Evidence is at `vv_work:ComputeFeatureReferenceMisorientationsFilter/ReadMe.md`, `vv_work:ComputeFeatureReferenceMisorientationsFilter/comparison/results.md`, `vv_work:ComputeFeatureReferenceMisorientationsFilter/comparison/results.json`, `vv_work:ComputeFeatureReferenceMisorientationsFilter/comparison/run_exit_codes.txt` and `vv_work:ComputeFeatureReferenceMisorientationsFilter/comparison/mode1_emulation_results.md`. Input generators are `vv_work:ComputeFeatureReferenceMisorientationsFilter/scripts/generate_fixture_inputs.py`, `vv_work:ComputeFeatureReferenceMisorientationsFilter/scripts/generate_smallin100_input.py` and `vv_work:ComputeFeatureReferenceMisorientationsFilter/scripts/generate_mode1_emulation_inputs.py`. -### Class 5 — Expert-visual +Commit `4ec21e93f` (2026-09-20, PR #1568) changes the algorithm after this comparison. It adds block reads, cached references, participating phase/Laue checks and background exclusion during center selection. A comparison with the current algorithm is required. -N/A. +**Release-source evidence** -## Second-engineer oracle review +Each release branch below was read directly. Paths are `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeFeatureReferenceMisorientations.cpp` and `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeFeatureReferenceMisorientationsFilter.cpp`. -- **Reviewer:** *Pending — recommend Joey Kleingers or another OA-domain engineer review:* - - *The Class 1 hand-derivations in the per-fixture summary table for plausibility (the 7 fixtures are small enough to walk through in ~30 minutes).* - - *The Class 4 invariant set for completeness — are there other properties this algorithm must satisfy?* - - *The decision to retire the `compute_feature_reference_misorientation.tar.gz` archive (Small-IN100 exemplar regression test) in favor of inline data fixtures.* -- **Date:** *YYYY-MM-DD (pending)* +| Release branches | D2: required AvgQuats in Mode 1 | D3: cell index into AvgQuats | D4: conversion from FeatureIds | D5 and D6: unchecked phase/Laue indices | +|---|---|---|---|---| +| `upstream/release/v7.0.0`, `v7.0.1`, `v7.0.2`, `v7.0.3`, `v7.2.0`, `v7.2.1`, `v7.2.2`, `v7.4.0` | Present | Present | Present | Present | +| `upstream/release/v7.4.1` | Fixed | Fixed | Present | Present | +| `upstream/release/v7.4.2` | Fixed | Fixed | Fixed | Present | -## Archive retirement +PR #1504 (`f7fec9819`) fixes D2 and D3; PR #1682 (`5eb69f359`) fixes D4; PR #1568 (`4ec21e93f`) fixes D5 and D6. DREAM3D-NX v7.4.2 was released on 2026-08-28. The D5 and D6 fixes will be in the DREAM3D-NX release after v7.4.2. -### Retired archive: `compute_feature_reference_misorientation.tar.gz` +The DREAM.3D v6.5.171 source at `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/FindFeatureReferenceMisorientations.cpp` confirms D2, D5 and D6. Its Mode 1 reference lookup uses the cell quaternion array, so D3 does not apply. It has no SIMPLNX conversion path, so D4 does not apply. The same source establishes float quaternion math, column-row-plane traversal, and the absence of the additional center-coordinate output. -| Field | Value | -|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------| -| **Archive** | `compute_feature_reference_misorientation.tar.gz` | -| **SHA512** | `6ea9c04ca5b0c0439573b5a14bda63592181c6badb4dd325b542fb97ff2a5d492e83d2bac1bf5999612cbdb7697ec48e321549427470f1f23ccd37921c6a95f1` | -| **Status** | **Retired** 2026-06-01 during this V&V cycle. `download_test_data()` entry removed from `src/Plugins/OrientationAnalysis/test/CMakeLists.txt`. | -| **Retired tests** | `ComputeFeatureReferenceMisorientationsFilter_AverageMisorientation` (Mode 0 regression against Small IN100 exemplar) and `ComputeFeatureReferenceMisorientationsFilter_EuclideanDistance` (Mode 1 regression against Small IN100 exemplar). | +## Oracle provenance (Classes 2, 3, 5 only) -### Why the archive was retired +Not applicable. The independent oracle uses Classes 1 and 4, encoded directly in the test source. Legacy and local proof outputs explain migration differences; they do not supply expected test values. -The archive contained a Small-IN100-derived `.dream3d` with both input data and pre-computed exemplar `Mask`-equivalent output arrays. The two retired tests compared filter output against those exemplar arrays via `UnitTest::CompareArrays` with a `1e-4` epsilon. After EbsdLib 2.4.1 introduced the `2·atan2(|v|, w)` precision fix in `CubicOps::calculateMisorientationInternal`, the exemplar arrays drifted by `2× to 10× epsilon` (2×10⁻⁴ to 1×10⁻³) from the new SIMPLNX output. The failure mode was the **circular-oracle pattern** described in `docs/vv_templates/oracle_classes.md` §"What is NOT an oracle": - -> *"**The filter's own output**, captured on a previous date and saved as a 'golden' exemplar. This is circular: any bug present at capture time becomes the new 'correct' answer, and the test then confirms the bug forever."* - -The exemplar arrays in the archive were generated from a SIMPLNX run at some unrecorded point in the past (the V&V cycle's discovery phase could not identify the generating pipeline, commit, or engineer — that history was lost). Regenerating the exemplar from current post-EbsdLib-2.4.1 SIMPLNX output would have re-instated the circular oracle. - -Instead, the V&V cycle replaced the regression-against-exemplar tests with the Class 1 + Class 4 data fixtures documented above. Coverage assessment: - -- **Algorithmic paths covered:** 7 of 8 directly, 1 (cancel) implicit. Both `ReferenceOrientation` modes covered; both 2D and 3D coord arithmetic covered. -- **Property coverage:** Monotonicity, range bounds, skip-condition correctness, per-feature averaging formula, background handling, zero-count handling — all asserted via `ClassFourInvariants`. - -**What's lost by retiring the archive:** -- Scale testing (Small IN100 ≈ 150K voxels vs. data fixtures ≤ 18 voxels). We won't catch O(N²) regressions at scale via unit tests. -- Real-world feature-size-distribution sanity check. +## Second-engineer oracle review -The user / V&V lead's decision was that scale testing is a Tier-1 integration concern (e.g., the canonical `(04) Small IN100 Crystallographic Statistics.d3dpipeline` pipeline check), not a unit-test concern, and the trade-off favors clean analytical correctness over circular-oracle regression coverage. +- **Reviewer:** Pending. +- **Date:** Pending. -### Regenerated to fix a circular-oracle situation? +## Regenerated to fix a circular-oracle situation? -**Yes** — see the §"Archive retirement" subsection. The 2026-06-01 V&V cycle replaced the circular-oracle exemplar archive with inline Class 1 + Class 4 oracles. The pre-existing archive is retired (not regenerated); future engineers should NOT re-introduce a Small-IN100 regression test for this filter without explicit V&V justification. +No archive was regenerated. PR #1629 replaces comparisons against saved SIMPLNX output with inline Class 1 expectations and Class 4 invariants. The old archive's manual reasonableness review does not make its SIMPLNX-generated arrays an independent numerical oracle. The retired archive remains useful as a real EBSD regression dataset; the current default fixtures use small analytical inputs. diff --git a/src/Plugins/OrientationAnalysis/vv/provenance/ComputeIPFColorsFilter.md b/src/Plugins/OrientationAnalysis/vv/provenance/ComputeIPFColorsFilter.md new file mode 100644 index 0000000000..576f517585 --- /dev/null +++ b/src/Plugins/OrientationAnalysis/vv/provenance/ComputeIPFColorsFilter.md @@ -0,0 +1,64 @@ +# Exemplar Archive Provenance: so3_cubic_high_ipf_001.tar.gz + +This archive supplies the released-version comparison input. ComputeIPFColorsFilter tests use an inline analytical fixture instead of its stored colors. + +## Archive identity + +| Field | Value | +|---|---| +| **Archive** | `so3_cubic_high_ipf_001.tar.gz` | +| **SHA512** | `dfe4598cd4406e8b83f244302dc4fe0d4367527835c5ddd6567fe8d8ab3484d5b10ba24a8bb31db269256ec0b5272daa4340eedb5a8b397755541b32dd616b85` | +| **Used by tests** | `OrientationAnalysis::CreateEnsembleInfoFilter: Valid filter execution`; retired from `OrientationAnalysis::ComputeIPFColors` | +| **Generated by** | Michael Jackson (inferred from PR #399) | +| **Generated on** | 2022-12-28 to 2023-01-01 (inferred from PR #399) | +| **Generated at commit** | `110539ab4` (PR #399 merge, inferred) | + +The archive name and SHA512 match `src/Plugins/OrientationAnalysis/test/CMakeLists.txt`. The first registration is in PR #399. + +## How it was generated + +The removed archive test identifies SIMPL/DREAM.3D as the producer of `so3_cubic_high_ipf_001.dream3d`. The file contains 343,963 cubic-phase cells and an embedded legacy RGB array. The comparison pipelines read its Euler Angles, Phases and Crystal Structures, then compute TSL colors for reference direction [0,0,1] with masking disabled. The current test builder `BuildAnalyticalDataset()` instead constructs six cells with explicit Euler angles, phases and boolean/uint8 masks in C++. + +The inline fixture has Crystal Structures `[Unknown, Cubic_High, Hexagonal_High]` and Phases `[1,1,2,1,0,2]`. Its Euler triples are `(0,0,0)`, `(0.3,0.4,0.5)`, `(0.1,0.2,0.3)`, `(0.3,0.4,0.5)`, `(0.3,0.4,0.5)` and `(0.7,0.8,0.9)` radians. Cell 3 is masked; cell 4 has the unknown structure. The filter does not use geometry coordinates, so origin and spatial-axis asymmetry do not affect this oracle. + +The comparison artifacts are `vv_work:ComputeIPFColorsFilter/pipelines/ipf_6_5_171.json`, `vv_work:ComputeIPFColorsFilter/pipelines/ipf_nx.d3dpipeline`, and the refreshed release pipelines generated by `vv_work:ComputeIPFColorsFilter/scripts/build_release_matrix.py`. The comparison reads integer RGB arrays and counts tuple/component differences in `vv_work:ComputeIPFColorsFilter/scripts/compare_release_matrix.py`. + +## Canonical oracle output + +| DataPath | Source of expected values | +|---|---| +| `/DataContainer/CellData/IPFColors` | Inline Class 1: masked and invalid-structure cells are black; the identity cubic [001] cell has R at least 250 and G = B = 0. | +| `/DataContainer/CellData/IPFColors` | Class 2 routing reference: direct EbsdLib calls supply exact RGB values for cells 0, 1, 2 and 5. | +| `/DataContainer/CellData/IPF_Unit` and `/DataContainer/CellData/IPF_NonUnit` | Class 4: all bytes agree after normalization of [0,0,1] and [0,0,5]. | +| `/ImageDataContainer/CellData/IPF Colors` | Embedded legacy comparison reference only; not an independent correctness oracle. | + +`src/Plugins/OrientationAnalysis/test/ComputeIPFColorsTest.cpp` contains 10 test cases. Its nine regular cases cover the inline oracle, mask variants, normalization, upper and lower phase bounds, color-key routing, tuple-count rejection and SIMPL conversion. All pass in CI at `419f9db35` on Linux, macOS and Windows. The test source and algorithm sources match that CI revision. + +## Oracle provenance (Classes 2, 3, 5 only) + +The Class 2 reference is `EbsdLibReferenceColor()` in `src/Plugins/OrientationAnalysis/test/ComputeIPFColorsTest.cpp`. It calls the linked C++ EbsdLib directly with the fixture Euler angles, crystal structure, normalized direction and color key. `vcpkg.json` requires EbsdLib 3.1.2 or later. Inputs are deterministic; no random seed or external oracle script is used. This reference checks SIMPLNX routing and RGB packing, not the color mathematics shared with EbsdLib. + +The test name retains `Class 1/2/3 Oracle`, but its red-corner assertion uses a literal expectation without a paper citation. The report classifies that assertion as analytical. + +Run against DREAM.3D v6.5.171 on 2026-07-09: 343,949 of 343,963 RGB tuples match exactly; all match within one channel level. SIMPLNX matches the embedded legacy array exactly. `vv_work:ComputeIPFColorsFilter/README.md` identifies the stock PipelineRunner; `vv_work:ComputeIPFColorsFilter/results/three_way_comparison.txt` records the results. + +Run against DREAM.3D v6.5.171 on 2026-09-17: 343,949 of 343,963 RGB tuples match exactly for each of DREAM3D-NX v7.4.1 and v7.4.2; all match within one channel level. Both releases match each other and the embedded legacy colors exactly. Results are in `vv_work:ComputeIPFColorsFilter/results/release_matrix/comparison.md` and `vv_work:ComputeIPFColorsFilter/results/release_matrix/comparison.json`. The legacy runner log reports SIMPLib 1.2.828 in `vv_work:ComputeIPFColorsFilter/results/release_matrix/logs/legacy_6_5_171.stdout.txt`. + +The current algorithm changed after that comparison: `4ec21e93f`, 2026-09-20, PR #1568, introduced Direct/Scanline execution and negative-phase validation. The release comparison does not establish current-algorithm parity. The earlier 2026-07-09 run also predates `ce6b67b5f`, 2026-07-13, PR #1666. + +Release-source inspection used `git show` on all ten `upstream/release/v` refs from v7.0.0 through v7.4.2. Paths below are relative to `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/`. + +| Defect | Release-source evidence | Correction | +|---|---|---| +| D2: counter race | `ComputeIPFColors.hpp` declares plain `int32_t m_PhaseWarningCount` in v7.0.0 through v7.4.1 and `std::atomic_int32_t` in v7.4.2; the worker increments it in `ComputeIPFColors.cpp`. | PR #1666 (`ce6b67b5f`) changes the counter to atomic. Current Direct retains that type; Scanline uses a serial local counter. | +| D3: negative phase | `ComputeIPFColors.cpp` in all ten releases checks only the upper phase bound before Crystal Structures access. | PR #1568 (`4ec21e93f`) adds `-48001` in Direct and Scanline. | + +DREAM.3D tag `v6.5.171`, `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/GenerateIPFColors.cpp`, has both defects: its worker reads Crystal Structures with no lower phase bound and increments a shared counter from `tbb::parallel_for`. `GenerateIPFColors.h` declares that counter as plain `int32_t`. + +## Second-engineer oracle review + +The existing report records Michael Jackson as technical authority on 2026-07-16. Its sign-off names no second engineer. + +## Regenerated to fix a circular-oracle situation? + +No replacement archive was generated for this filter. PR #1666 removed the legacy-color comparison test and supplied the inline analytical fixture. The original archive remains registered because CreateEnsembleInfoTest uses it. The released-version comparison uses the archive to explain differences, not to define correct colors. diff --git a/src/Plugins/OrientationAnalysis/vv/provenance/ComputeKernelAvgMisorientationsFilter.md b/src/Plugins/OrientationAnalysis/vv/provenance/ComputeKernelAvgMisorientationsFilter.md index 1f107a9de1..cebdb5e7c9 100644 --- a/src/Plugins/OrientationAnalysis/vv/provenance/ComputeKernelAvgMisorientationsFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/provenance/ComputeKernelAvgMisorientationsFilter.md @@ -1,183 +1,155 @@ -# Exemplar Archive Provenance: Inlined in Test +# Exemplar Archive Provenance: 6_6_stats_test_v2.tar.gz -This sidecar records how the test data used by `ComputeKernelAvgMisorientationsFilter`'s Class 1 and Class 4 unit tests was generated. It is the answer to "where did this hand-built data come from?" +This sidecar records the retired shared archive, current inline oracles, release-source checks and the dated legacy comparison. -The test data is **inlined** in the test source — there is no separate tar.gz archive specifically owned by this filter, no `download_test_data()` entry for the new fixtures, and no `.dream3d` exemplar file to fetch. (The shared archive `6_6_stats_test_v2.tar.gz` is still referenced in `src/Plugins/OrientationAnalysis/test/CMakeLists.txt`, but for use by `AlignSectionsMutualInformationTest`, `ComputeShapesFilterTest`, and `ComputeSchmidsTest` — not for this filter.) +## Archive identity ---- +| Field | Value | +|---|---| +| **Archive** | `6_6_stats_test_v2.tar.gz` — retired for this filter; current fixtures are inline. | +| **SHA512** | `e84999dec914d81efce4fc4237c49c9bf32e48381b1e79f58aa4df934f0d7606cd7a948f9a5e7b17a126a7944cc531b531cfdc70756ca3e2207b20734e089723` | +| **Used by tests** | No current KAM test consumes this archive. The six `Class 1` cases, two `Class 4` cases and `Bounded Cache Full-Depth Kernel` construct their own inputs. | +| **Generated by** | Jared Duffey (inferred from PR #1124) | +| **Generated on** | 2024-11-01 to 2025-02-07 (inferred from PR #1124) | +| **Generated at commit** | `dc4544512` (PR #1124 merge, inferred) | -## Archive identity +`src/Plugins/OrientationAnalysis/test/CMakeLists.txt` registers this name and SHA512. The first registration is `dc4544512`, which replaces `6_6_stats_test.tar.gz` with the v2 archive. The identity fields above describe the shared archive, not the inline fixtures. -| Field | Value | -|-------------------|------------------------------------------------------------------------------------------------------------------------------------------------| -| **Archive** | Inlined (no separate archive) | -| **SHA512** | N/A | -| **Used by tests** | `OrientationAnalysis::ComputeKernelAvgMisorientationsFilter: Class 1 - Uniform 2D Single Feature` | -| | `OrientationAnalysis::ComputeKernelAvgMisorientationsFilter: Class 1 - 1D x-axis Gradient` | -| | `OrientationAnalysis::ComputeKernelAvgMisorientationsFilter: Class 1 - 1D z-axis Gradient (3D path)` | -| | `OrientationAnalysis::ComputeKernelAvgMisorientationsFilter: Class 1 - Multi-Feature Multi-Voxel + Background` | -| | `OrientationAnalysis::ComputeKernelAvgMisorientationsFilter: Class 1 - Per-Voxel Mode (use_feature_ids = false)` *(new 2026-07-15, #1613)* | -| | `OrientationAnalysis::ComputeKernelAvgMisorientationsFilter: Class 1 - Per-Voxel Mode Two-Phase Gates` *(new 2026-07-15, #1613)* | -| | `OrientationAnalysis::ComputeKernelAvgMisorientationsFilter: Class 4 - Mode Equivalence on Single Feature` *(new 2026-07-15, #1613)* | -| | `OrientationAnalysis::ComputeKernelAvgMisorientationsFilter: Class 4 - Invariants` (3 SECTIONs) | -| **Generated by** | Claude (Anthropic) under direction of Michael Jackson | -| **Generated on** | 2026-06-03 (Class 1/4 base fixtures); 2026-07-15 (per-voxel + mode-equivalence fixtures added for issue #1613 on branch `topic/kam_ignore_feature_ids`) | +All pass in CI at `419f9db35`. The supplied CI result covers Linux, macOS and Windows in-core builds. ## How it was generated -The dataset is a hand-rolled in-memory `DataStructure` designed as a **Class 1 (Analytical) oracle** with a paired **Class 4 (Invariant)** check. As of the 2026-07-15 reopen (issue #1613) it systematically covers the eight algorithmic paths in `ComputeKernelAvgMisorientations::FindKernelAvgMisorientationsImpl::convert()` — see the V&V report's Code path coverage table for the full enumeration and per-test mapping: - -1. **Focal-valid gate** (`featureIds[point] > 0 && cellPhases[point] > 0`) → enter the kernel. -2. **Kernel cell out-of-bounds** (signed boundary clamp on `zIdx`/`yIdx`/`xIdx`) → `continue`. -3. **Per-grain** (`use_feature_ids=true`) in-bounds + feature-id match → accumulate. -4. **Per-grain** in-bounds + feature-id mismatch → skip. -5. **Per-voxel** (`use_feature_ids=false`) in-bounds + `featureId>0` + phase match → accumulate. -6. **Per-voxel** neighbor `featureId==0` → skip. -7. **Per-voxel** neighbor phase mismatch → skip. -8. **Focal-invalid** (`featureIds[point] == 0 || cellPhases[point] == 0`, reached via `else` since commit `7f9cddc7d`) → KAM = 0 directly. - -A 9th algorithmic path (`numVoxel == 0` fallback) existed at the time these fixtures were designed (2026-06-03/2026-07-15) and was dead code in practice (the focal voxel always self-contributes in both modes) — not exercised by design. The review-driven cleanup pass (commit `7f9cddc7d`, 2026-07-16) removed that fallback from SIMPLNX as unreachable dead code, so the enumeration above is now the complete, current path list (8 of 8, all exercised); no fixture changes were needed since no fixture ever targeted the removed path. - -### Scaffold structure - -The `AnalyticalFixtures` namespace at the top of `ComputeKernelAvgMisorientationsTest.cpp` provides a `CreateScaffold(nX, nY, nZ, numCrystalStructures)` helper that constructs: - -- A single `ImageGeom` named `ImageGeometry` with the requested dimensions, spacing `{1, 1, 1}`, and origin `{0, 0, 0}`. -- A `CellData` `AttributeMatrix` with tuple shape `{nZ, nY, nX}` (the SIMPLNX storage convention for 3D cell-level arrays). -- Three input cell arrays initialized to default values: `FeatureIds` (int32, default 1), `Phases` (int32, default 1), `Quats` (float32, 4 components, default identity `(0, 0, 0, 1)`). -- An `EnsembleData` `AttributeMatrix` with tuple shape `{numCrystalStructures}` (defaulting to 2: index 0 = sentinel `999`, index 1 = `Cubic_High` = 1). -- A `CrystalStructures` ensemble array set to `[999, 1]` by default. - -Test cases use `SetCellQuat(td, cellIdx, q)` to overwrite individual cell quaternions and direct `(*td.featureIds)[i] = N` / `(*td.cellPhases)[i] = N` assignments to set up multi-feature / background scenarios. `BuildArgs(kernelRadius)` constructs the `Arguments` object for the filter with the standard input/output paths. +The shared archive contains Small IN100 reconstruction data and stored filter outputs. Current KAM tests instead call `AnalyticalFixtures::CreateScaffold()` in `src/Plugins/OrientationAnalysis/test/ComputeKernelAvgMisorientationsTest.cpp` to construct an Image Geometry, cell arrays and an ensemble array. `QuatFromPhi1Deg()` constructs `(0,0,sin(φ1/2),cos(φ1/2))` quaternions from specified angles. Expected values come from hand averages and invariants, independently of filter output. + +The base Class 1 and Class 4 fixtures replace the archive consumer in `ec22ac375` (PR #1631). `a96b48332` (PR #1674) adds the same-phase mode fixtures. `4ec21e93f` (PR #1568) adds memory-planning, phase/Laue bounds, maximum-radius, empty-image and bounded-cache checks. + +**Fixture construction** + +| Item | Definition | +|---|---| +| Geometry | `ImageGeometry`, dimensions supplied by each fixture, spacing `{1,1,1}`, origin `{0,0,0}`. | +| Cell group | `ImageGeometry/CellData`, tuple shape `{nZ,nY,nX}`. | +| Cell arrays | `FeatureIds` and `Phases`: int32, one component; `Quats`: float32, four components in `(x,y,z,w)` order. | +| Ensemble group | `ImageGeometry/EnsembleData`; `CrystalStructures` defaults to `[999,1]`, with cubic phase one. | +| Two-phase fixture | `CrystalStructures=[999,1,1]`; the two phase IDs share cubic symmetry but remain distinct phase labels. | +| Output | `ImageGeometry/CellData/KernelAverageMisorientationsOut`, float32, one component, in degrees. | -### Orientation convention +The X and Z line fixtures use different dimensions and radii to distinguish axis indexing. The multi-feature fixtures distinguish feature IDs and phase IDs. Geometry origin does not enter the kernel calculation; it uses integer cell coordinates and radii. -All Class 1 fixtures use **pure φ1 Bunge ZXZ Euler rotations** `(φ1, 0, 0)` with `Φ = φ2 = 0`. These collapse to a single rotation about the z-axis. For cubic Laue class (`Cubic_High`), the symmetry group's 4-fold proper rotation about each crystallographic axis includes the c-axis (z), which reduces φ1 differences modulo 90°. By keeping φ1 differences ≤ 45°, the symmetry-reduced minimum-rotation magnitude is exactly `|Δφ1|`. This makes the oracle closed-form: no numerical sym-op search required, the expected misorientation between two cubic-phase cells is `|φ1_neighbor - φ1_focal|` in degrees. +**Legacy comparison inputs and execution** -The `QuatFromPhi1Deg(phi1)` helper produces `{0, 0, sin(phi1/2 rad), cos(phi1/2 rad)}` — the standard quaternion form of a pure z-axis rotation in the EbsdLib (x, y, z, w) Vector-Scalar order. +Run against DREAM.3D v6.5.171 on 2026-09-17: 0 of 6 KAM arrays match bit-for-bit. One of six cases has every cell within 0.01°. All six cases agree with a local proof version within 0.000001°. These results describe the implementation used on that date. -### Fixture-by-fixture derivation +`vv_work:ComputeKernelAvgMisorientationsFilter/ReadMe.md` identifies the stock application and its `SIMPLib Version 1.2.828.f45085c83` runner banner. It identifies the SIMPLNX comparison source as `8b80721c20853d891d5040f9134315af1d93b2b6`. All cases use the default same-feature mode. -#### Fixture 1 — `Class 1 - Uniform 2D Single Feature` +The synthetic input is a 12×12×12 image with eight 6×6×6 feature blocks and one cubic phase. `vv_work:ComputeKernelAvgMisorientationsFilter/scripts/make_synthetic_input.py` uses NumPy seed 1613, a random-axis base rotation of 5–25° per feature and a random-axis perturbation of 0–3° per cell. The production input is an 80×80×40 crop of Small IN100 with 256,000 cells and 260 features. `vv_work:ComputeKernelAvgMisorientationsFilter/scripts/make_production_input.py` copies only FeatureIds, Phases, Quats and Crystal Structures from the shared data; it excludes the old KAM output. -- 3×3×1 image, single feature (id=1), single phase (cubic), all cells share the identity quaternion `(0, 0, 0, 1)`. -- Kernel radius `{1, 1, 0}` → 3×3×1 kernel (up to 9 cells per kernel, fewer at edges). -- Expected: every cell's KAM = 0° (all in-kernel neighbors have the same orientation). -- Tests: focal-valid gate (path 1), feature-id-match (path 2), 2D boundary clamp (path 4). +`vv_work:ComputeKernelAvgMisorientationsFilter/scripts/make_pipelines.py` supplies equivalent pipelines for three kernel radii on each input. `vv_work:ComputeKernelAvgMisorientationsFilter/scripts/compare_kam.py` compares the outputs and independently counts same-feature neighbors for the correct and incorrect X bounds. -#### Fixture 2 — `Class 1 - 1D x-axis Gradient` +| Case | Kernel | Different cells / total vs DREAM.3D v6.5.171 | Maximum absolute difference (°) | Mean absolute difference (°) | Cells above 0.01° | Maximum difference from local proof version (°) | +|---|---|---|---|---|---|---| +| `synth12_k111` | `{1,1,1}` | 1727 / 1728 | 0.0071352 | 0.00073522 | 0 | 0.00000071526 | +| `synth12_k112` | `{1,1,2}` | 1728 / 1728 | 0.46176 | 0.046905 | 1024 | 0.00000047684 | +| `synth12_k211` | `{2,1,1}` | 1728 / 1728 | 0.68350 | 0.051496 | 1040 | 0.00000095367 | +| `smallin100_k111` | `{1,1,1}` | 255969 / 256000 | 0.055953 | 0.0015421 | 2281 | 0.00000095367 | +| `smallin100_k112` | `{1,1,2}` | 255987 / 256000 | 1.1429 | 0.024898 | 160143 | 0.00000095367 | +| `smallin100_k211` | `{2,1,1}` | 255980 / 256000 | 1.1580 | 0.023831 | 153828 | 0.00000047684 | -- 5×1×1 image (single row), single feature, single phase. -- Per-cell φ1: `[0°, 5°, 10°, 15°, 20°]`. -- Kernel radius `{1, 0, 0}` → 1D kernel along x. -- Expected per-cell KAM derivation: - - cell 0: neighbors `{self=0°, x+1=5°}` → misos `{0, 5}` → avg `5/2 = 2.500°` - - cell 1: neighbors `{x-1=0°, self=5°, x+1=10°}` → misos `{5, 0, 5}` → avg `10/3 ≈ 3.3333°` - - cell 2: neighbors `{x-1=5°, self=10°, x+1=15°}` → misos `{5, 0, 5}` → avg `10/3 ≈ 3.3333°` - - cell 3: neighbors `{x-1=10°, self=15°, x+1=20°}` → misos `{5, 0, 5}` → avg `10/3 ≈ 3.3333°` - - cell 4: neighbors `{x-1=15°, self=20°}` → misos `{5, 0}` → avg `5/2 = 2.500°` -- Tests: averaging arithmetic + 1D x-stride boundary clamp. +FeatureIds, Phases and Quats match bit-for-bit across all three outputs in each case. The comparison evidence is `vv_work:ComputeKernelAvgMisorientationsFilter/comparison/summary.md`, `vv_work:ComputeKernelAvgMisorientationsFilter/comparison/legacy_comparison_report.md` and the six `comparison/.txt` files. -#### Fixture 3 — `Class 1 - 1D z-axis Gradient (3D path)` +For each asymmetric synthetic case, the wrong X bound changes the divisor in 1,152 cells. Every synthetic cell above the 0.01° threshold belongs to that set. The corresponding Small IN100 divisor changes affect 233,016 cells for `{1,1,2}` and 227,760 cells for `{2,1,1}`. Equal X and Z radii leave kernel membership unchanged. -- 1×1×3 image (single column in z), single feature, single phase. -- Per-plane φ1: `[0°, 10°, 20°]`. -- Kernel radius `{0, 0, 1}` → 1D kernel along z. -- Expected per-cell KAM: - - plane 0 (focal=0°): neighbors `{self=0°, z+1=10°}` → misos `{0, 10}` → avg `10/2 = 5.000°` - - plane 1 (focal=10°): neighbors `{z-1=0°, self=10°, z+1=20°}` → misos `{10, 0, 10}` → avg `20/3 ≈ 6.6667°` - - plane 2 (focal=20°): neighbors `{z-1=10°, self=20°}` → misos `{10, 0}` → avg `10/2 = 5.000°` -- Tests: 3D outer-loop z-path + z-stride boundary clamp. +The local proof version combines the one-line X-bound correction (`9186bc716`) and CubicOps precision correction (`43b8063f3`). The patch evidence is `vv_work:ComputeKernelAvgMisorientationsFilter/notes/legacy_patches_9186bc716_43b8063f3.diff.txt`. The maximum residual below 0.000001° applies to this local proof version, not to stock DREAM.3D v6.5.171. -#### Fixture 4 — `Class 1 - Multi-Feature Multi-Voxel + Background` +The Small IN100 symmetric-kernel result disproves a universal 0.01° bound. Its mean absolute difference is 0.0015421°, not 0.005–0.05°. Both identical and nearly identical distinct pairs contribute to precision differences. The measurements do not support attributing every difference only to focal self-misorientation. -- 6×1×1 image. Cell layout (x = 0 to 5): - - cell 0: featureId=1, phase=1, φ1=0° - - cell 1: featureId=1, phase=1, φ1=10° - - cell 2: featureId=2, phase=1, φ1=0° - - cell 3: featureId=2, phase=1, φ1=20° - - cell 4: featureId=0, phase=0 (background — orientation irrelevant) - - cell 5: featureId=1, phase=1, φ1=30° -- Kernel radius `{1, 0, 0}`. -- Expected per-cell KAM derivation: - - cell 0 (F1, φ1=0°): kernel `{x=0 self, x=1 (F1)}`. Same-feat misos `{|0-0|, |10-0|} = {0, 10}`. sum=10, div=2 → KAM = 5.000°. - - cell 1 (F1, φ1=10°): kernel `{x=0 (F1), x=1 self, x=2 (F2 - SKIP)}`. Same-feat misos `{|0-10|, |10-10|} = {10, 0}`. sum=10, div=2 → KAM = 5.000°. - - cell 2 (F2, φ1=0°): kernel `{x=1 (F1 - SKIP), x=2 self, x=3 (F2)}`. Same-feat misos `{|0-0|, |20-0|} = {0, 20}`. sum=20, div=2 → KAM = 10.000°. - - cell 3 (F2, φ1=20°): kernel `{x=2 (F2), x=3 self, x=4 (F0 - SKIP because F0 ≠ F2)}`. Same-feat misos `{|0-20|, |20-20|} = {20, 0}`. sum=20, div=2 → KAM = 10.000°. - - cell 4 (F0/P0, background): focal-invalid (path 5) → KAM = 0° exactly. - - cell 5 (F1, φ1=30°): kernel `{x=4 (F0 - SKIP), x=5 self}`. Same-feat misos `{|30-30|} = {0}`. sum=0, div=1 → KAM = 0.000°. -- Tests: multi-voxel within-feature averaging (cells 0–3), multi-feature mismatch skip (cells 1, 2, 5 see different-feature in-bounds neighbors), background skip path (cell 4 — also asserted with `REQUIRE(kam[4] == 0.0f)` exact-zero check), isolated single-cell feature (cell 5). +The original 2026-07-15 input generator is absent from the comparison archive. The 2026-09-17 generator implements the recorded recipe but produces different cell values. For the symmetric synthetic case, its signed split is 907 cells with legacy above SIMPLNX, 820 below and one equal. The earlier split of 928/800 is not the result of this archived input. -#### Fixture 5 — `Class 1 - Per-Voxel Mode` (new 2026-07-15, issue #1613) +Algorithm source changed after the comparison date: -- Same 6×1×1 layout as Fixture 4 (featureIds `[1,1,2,2,0,1]`, phases `[1,1,1,1,0,1]`, φ1 `[0,10,0,20,–,30]°`), but run with `use_feature_ids = false`. -- Kernel radius `{1,0,0}`. Per-voxel gate: neighbor contributes iff `featureId>0 && phase==focalPhase` (feature boundaries ignored; `featureId=0` and phase-mismatched cells excluded). -- Expected per-cell KAM: - - cell 0: `{self=0, x1(F1,P1)=|10−0|}` → 10/2 = **5.0** - - cell 1: `{x0=|0−10|, self=0, x2(F2,P1)=|0−10|}` → 20/3 ≈ **6.6667** — diagnostic cell (per-grain gives 5.0; per-voxel includes the cross-feature same-phase x2) - - cell 2: `{x1(F1,P1)=|10−0|, self=0, x3(F2,P1)=|20−0|}` → 30/3 = **10.0** - - cell 3: `{x2(F2,P1)=|0−20|, self=0}`; x4 excluded (`featureId=0`) → 20/2 = **10.0** - - cell 4: focal-invalid (`featureId=0, phase=0`) → **0.0** exactly - - cell 5: `{self=0}`; x4 excluded (`featureId=0`) → 0/1 = **0.0** -- Expected array `{5.0, 20/3, 10.0, 10.0, 0.0, 0.0}`. Exercises paths 5 (per-voxel include) and 6 (`featureId=0` exclude) plus the unchanged focal-invalid path 8. +| Commit | Date | Change | +|---|---|---| +| `4ec21e93f` | 2026-09-20 | PR #1568 splits execution into Direct and Scanline, adds input validation and bounded memory planning, and clamps maximum-radius traversal. | +| `a33c370ca` | 2026-09-25 | PR #1697 changes progress-message handling. | -#### Fixture 6 — `Class 1 - Per-Voxel Mode Two-Phase Gates` (new 2026-07-15, issue #1613) +The report therefore does not use the 2026-09-17 numerical results as a comparison of the current algorithm. -- 5×1×1, every cell its own feature, two cubic phases (ensemble `CrystalStructures = [999, 1, 1]`). featureIds `[1,2,3,4,0]`, phases `[1,1,2,1,1]`, φ1 `[0,10,20,30,40]°`. Kernel `{1,0,0}`, `use_feature_ids = false`. -- Expected per-cell KAM: - - cell 0: `{self=0, x1(P1)=|0−10|}` → 10/2 = **5.0** (per-grain would give 0.0 — every feature is a single cell) - - cell 1: `{x0(P1)=|10−0|, self=0}`; x2 skipped (phase 2 ≠ 1) → 10/2 = **5.0** - - cell 2: `{self=0}`; x1, x3 skipped (phase 1 ≠ 2) → 0/1 = **0.0** - - cell 3: `{self=0}`; x2 skipped (phase), x4 skipped (`featureId=0`) → 0/1 = **0.0** - - cell 4: focal `featureId=0` (invalid) even though phase>0 → **0.0** exactly -- Expected array `{5.0, 5.0, 0.0, 0.0, 0.0}`. Exercises path 7 (phase-mismatch exclude), path 6 (`featureId=0` exclude), and confirms path 8's focal gate is unchanged in per-voxel mode (cell 4). +**Release-source evidence** -#### Fixture 7 — `Class 4 - Mode Equivalence on Single Feature` (new 2026-07-15, issue #1613) +Each of `upstream/release/v7.0.0`, `v7.0.1`, `v7.0.2`, `v7.0.3`, `v7.2.0`, `v7.2.1`, `v7.2.2`, `v7.4.0`, `v7.4.1` and `v7.4.2` was inspected. The filter sources are `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeKernelAvgMisorientationsFilter.cpp` and `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeKernelAvgMisorientations.cpp`. -- 3×3×3, single feature (id 1), single cubic phase, gradient φ1 = `2x + 3y + 4z` degrees (max 18° < 45° FZ bound). Full 3D kernel `{1,1,1}`. -- The fixture is built twice and run once with `use_feature_ids = true` and once with `false`. **Derived-truth invariant** (no hand-computed values): on single-feature single-phase data every neighbor passes both gates identically, so the two mode outputs must be bit-for-bit equal — `REQUIRE(kamPerGrain[i] == kamPerVoxel[i])` for all 27 cells, plus a guard that at least one cell is non-zero. Oracle-class-agnostic; holds regardless of the EbsdLib precision family. +| Releases | EbsdLib minimum in `vcpkg.json` | Cubic angle calculation at the corresponding EbsdLib tag | +|---|---|---| +| v7.0.0 through v7.0.3 | 1.0.35 | `acos(wmin)` | +| v7.2.0 | 1.0.37 | `acos(wmin)` | +| v7.2.1 | 1.0.38 | `acos(wmin)` | +| v7.2.2 | 1.0.39 | `acos(wmin)` | +| v7.4.0 | 1.0.40 | `acos(wmin)` | +| v7.4.1 | 2.1.0 | `acos(wmin)` | +| v7.4.2 | 3.1.2 | Explicit reduced vector and `2*atan2` | -#### Fixture 8 — `Class 4 - Invariants` (3 sub-sections) +The dependency source is `Source/EbsdLib/LaueOps/CubicOps.cpp`. EbsdLib `5c8c993` introduces the stable formula, present at v2.4.1 and v3.1.2. SIMPLNX `ec22ac375` upgrades the dependency from 2.4.0 to 3.0.0; the v7.4.2 manifest requires 3.1.2. This establishes the D1 affected range through DREAM3D-NX v7.4.1. -Three SECTIONs each constructing a fresh fixture inline: - -**Sub-section (i):** 3×3×3 uniform-identity-quaternion fixture, kernel `{1, 1, 1}`. Asserts every cell's KAM == 0 within `1e-4°` tolerance. Tests the 3D outer-loop path that none of the 4 Class 1 fixtures exercises end-to-end. - -**Sub-section (ii):** 3×1×1 fixture with cells `[F1P1(0°), F0P0(bg), F1P1(10°)]`, kernel `{1, 0, 0}`. Asserts `kam[1] == 0.0f` exactly (the background cell). This is the exactness invariant — no tolerance allowed. - -**Sub-section (iii):** Same 5×1×1 x-axis gradient as Fixture 2. Asserts three universal invariants: -- Non-negativity: `kam[i] >= 0` for all cells. -- Cubic upper bound: `kam[i] <= 62.8°` (Mackenzie cubic maximum disorientation). -- Non-triviality: at least one cell has `kam > 1e-4°` (sanity check that the algorithm actually ran rather than zeroing everything). +All inspected DREAM3D-NX algorithm snapshots use the correct X upper bound for ordinary radii (D2). All also contain unchecked phase/Laue lookups (D4) and signed radius-plus-one loop bounds (D5). The current common validator and both executors correct D4 and D5 in `4ec21e93f`; the fixes will be in the DREAM3D-NX release after v7.4.2. The same-phase parameter (D3) is absent through v7.4.1 and present in v7.4.2. -The Class 4 invariants are oracle-agnostic — they hold for any input, so they catch regressions even if specific Class 1 expected values were edited away. +The DREAM.3D `v6.5.171` source at `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/FindKernelAvgMisorientations.cpp` contains the X-bound typo, unchecked phase/Laue access and signed radius-plus-one expressions. Its `dataCheck()` validates array types and tuple counts but does not bound participating index values or maximum radii. `Source/OrientationLib/LaueOps/CubicOps.cpp` uses `acos(wmin)`. DREAM.3D v6.5.171 was affected by D1, D2, D4 and D5. ## Canonical oracle output -| DataPath | Source of expected values | -|---------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------| -| `/ImageGeometry/CellData/KernelAverageMisorientationsOut` | Class 1 analytical (closed-form `|Δφ1|` summed and divided per kernel + path-5 background→0 short-circuit) + Class 4 invariants (non-negativity, cubic max, exact-0 for background, uniform→0). | +No archived KAM array is canonical. The expected output is `/ImageGeometry/CellData/KernelAverageMisorientationsOut`, constructed analytically in the test source. + +| Test suffix | Geometry | Kernel | Expected values in degrees | +|---|---|---|---| +| `Class 1 - Uniform 2D Single Feature` | 3×3×1 | `{1,1,0}` | Nine zeros. | +| `Class 1 - 1D x-axis Gradient` | 5×1×1 | `{1,0,0}` | `[2.5,10/3,10/3,10/3,2.5]`. | +| `Class 1 - 1D z-axis Gradient (3D path)` | 1×1×3 | `{0,0,1}` | `[5,20/3,5]`. | +| `Class 1 - Multi-Feature Multi-Voxel + Background` | 6×1×1 | `{1,0,0}` | `[5,5,10,10,0,0]`. | +| `Class 1 - Per-Voxel Mode (use_feature_ids = false)` | 6×1×1 | `{1,0,0}` | `[5,20/3,10,10,0,0]`. | +| `Class 1 - Per-Voxel Mode Two-Phase Gates` | 5×1×1 | `{1,0,0}` | `[5,5,0,0,0]`. | +| `Bounded Cache Full-Depth Kernel` | 64×64×5 | `{0,0,4}` | Every cell in Z slice 0–4 has value `[10,7,6,7,10]`, respectively. | + +**Analytical derivation** + +All analytical orientations are pure Z rotations. Cubic symmetry reduces their angular difference modulo 90°. The fixture differences are at most 40°, so the minimum angle is `|Δφ1|`. For each focal cell, sum the admitted angles and divide by the admitted cell count, including the focal cell's zero contribution. + +The X gradient uses angles `[0,5,10,15,20]°`. Each endpoint includes itself and one neighbor, giving `5/2°`. Each interior cell includes two 5° differences and itself, giving `10/3°`. + +The Z gradient uses `[0,10,20]°`. The endpoints give `10/2=5°`; the middle gives `(10+0+10)/3=20/3°`. -The expected values are hard-coded into each TEST_CASE as `REQUIRE(kam[i] == Approx(expected[i]).margin(1e-3f))` checks for the gradient fixtures and `REQUIRE(kam[i] == 0.0f)` exact-zero checks for the background-cell case. Tolerance set to `1e-3°` — comfortably wider than EbsdLib 2.4.1+ precision (~`1e-5°` for the per-pair misorientation case) but tight enough to catch the legacy bug (D2) on asymmetric kernels and the EbsdLib 2.4.0 precision regression (D1) on focal-cell self-misorientation. +The six-cell fixture uses feature IDs `[1,1,2,2,0,1]`, phases `[1,1,1,1,0,1]`, and angles `[0,10,0,20,0,30]°`. The background angle is irrelevant. The following table shows the admitted angle sums and divisors in both modes. + +| Cell | Same-feature calculation | Same-phase calculation | +|---|---|---| +| 0 | `(0+10)/2=5` | `(0+10)/2=5` | +| 1 | `(10+0)/2=5`; reject cell 2 | `(10+0+10)/3=20/3`; admit cell 2 | +| 2 | `(0+20)/2=10`; reject cell 1 | `(10+0+20)/3=10`; admit cell 1 | +| 3 | `(20+0)/2=10`; reject background | `(20+0)/2=10`; reject background | +| 4 | Invalid focal feature: exactly zero | Invalid focal feature: exactly zero | +| 5 | Self only: zero | Self only: zero | + +The two-phase fixture uses feature IDs `[1,2,3,4,0]`, phases `[1,1,2,1,1]` and angles `[0,10,20,30,40]°`. Cells 0 and 1 admit each other and give 5°. Cell 2 rejects both adjacent phase-one cells. Cell 3 rejects phase two and background. Cell 4 is invalid despite its positive phase. + +The full-depth fixture uses slice angles `[0,5,10,15,20]°` in one feature. Each kernel includes all five slices at the same X/Y coordinate. The angle sums are `[50,35,30,35,50]°`; division by five gives `[10,7,6,7,10]°`. + +The gradient and mixed-feature assertions use a 0.001° margin. Uniform-zero assertions use 0.0001°. Background-zero assertions require exact equality. + +**Class 4 invariants** + +`Class 4 - Invariants` checks uniform zero, exact background zero, nonnegative KAM, the 62.8° cubic bound and a nonzero result on the X gradient. `Class 4 - Mode Equivalence on Single Feature` constructs a 3×3×3 gradient with `φ1=2x+3y+4z` degrees. Both modes admit the same neighbors, so every output must match bit-for-bit; a nonzero guard prevents an all-zero result from satisfying the comparison. ## Oracle provenance (Classes 2, 3, 5 only) -N/A — Class 1 and Class 4 oracles only. No reference-library invocation, no paper-figure reproduction, no expert-visual sign-off needed. +Not applicable. Classes 1 and 4 define the independent expected results in C++. Legacy outputs and the local proof version explain migration differences; they do not supply the oracle. ## Second-engineer oracle review -- **Reviewer:** *Pending — recommend Joey Kleingers or another OA-domain engineer review the cubic FZ argument (especially the "for pure z-rotations |Δφ1| ≤ 45° the disorientation IS |Δφ1|" claim) and the Fixture 4 multi-feature multi-voxel hand-derivation (6 per-cell expected values with mixed feature mismatch and background skip).* -- **Date:** *YYYY-MM-DD (pending)* -- **Skip reason** (if skipped): *N/A — second-engineer review is recommended; not yet performed.* - -## Regenerated to fix a circular-oracle situation? +- **Reviewer:** Pending. +- **Date:** Pending. -**Yes.** The inlined analytical dataset replaces the retired exemplar-comparison test. The pre-V&V test consumed the shared `6_6_stats_test_v2.tar.gz` archive's `KernelAverageMisorientations` exemplar, which was a circular oracle: the exemplar values were generated from a SIMPLNX build using EbsdLib < 2.4.1, where every cell's KAM was inflated by the spurious `~0.001–0.03°` self-misorientation precision noise documented as D1. Comparing the post-EbsdLib-2.4.1 SIMPLNX output against those exemplars would systematically fail by ~0.01–0.05° per cell (varies by focal-cell quaternion), which is what surfaced during the EbsdLib precision-cycle V&V work. +The requested review covers the cubic symmetry argument and the six-cell hand derivation. The report's existing sign-off names Michael Jackson but does not name a second engineer. -The inlined Class 1 + Class 4 fixtures use derived-truth oracles independent of any pre-existing SIMPLNX output, eliminating the circular oracle pattern. The shared archive `6_6_stats_test_v2.tar.gz` remains downloaded for the 3 other filter tests that still consume it (`AlignSectionsMutualInformation`, `ComputeShapes`, `ComputeSchmids`); only F#5's consumption line in `ComputeKernelAvgMisorientationsTest.cpp` was removed during this V&V cycle. +## Regenerated to fix a circular-oracle situation? -## Provenance log +The archive was not regenerated for this KAM verification. PR #1631 (`ec22ac375`) removes the KAM exemplar consumer and adds inline analytical and invariant checks. The current test-source comment identifies the former archived KAM output as a circular oracle. -- **2026-06-03** — Class 1 / Class 4 base fixtures (1–4, 8) authored; legacy exemplar test retired (circular-oracle fix). -- **2026-07-15** — Fixtures 5–7 (per-voxel mode, per-voxel two-phase gates, mode-equivalence invariant) added for issue #1613 on branch `topic/kam_ignore_feature_ids`; code paths re-enumerated 6→9 for the per-mode neighbor gate; runtime legacy A/B performed. -- **2026-07-16** — Review-driven cleanup pass, commits `7f9cddc7d` (REV) and the paired DOC commit on branch `topic/kam_ignore_feature_ids`: `getDataRefAs` for the parameter-validated `ImageGeom` (2 sites); kernel boundary clamps made signed with the neighbor index computed directly from the clamped `zIdx`/`yIdx`/`xIdx` (removing the vacuous unsigned checks and the separate `neighbor<0` guard); `LaueOps` list hoisted to a worker member built once per run in the constructor (shared read-only across parallel ranges, matching the `ComputeFeatureFaceMisorientationPerTriangleImpl` precedent); cancellation check moved from per-plane to per-row; progress increment floored at 1; the dead `numVoxel==0` fallback (former path 9) removed, folding the invalid-cell reset into an `else` branch. No fixture changes were required — all 9 existing TEST_CASEs pass unchanged on both in-core and out-of-core builds, confirming the cleanup is behavior-preserving. Code path enumeration updated from 9 (8 exercised) to 8 (8 exercised). +The shared archive remains registered for other filters. KAM correctness now depends on hand-derived values and invariants, rather than an output array generated by the implementation being checked. diff --git a/src/Plugins/OrientationAnalysis/vv/provenance/GroupMicroTextureRegionsFilter.md b/src/Plugins/OrientationAnalysis/vv/provenance/GroupMicroTextureRegionsFilter.md index d107a193d7..b21f9dd9b8 100644 --- a/src/Plugins/OrientationAnalysis/vv/provenance/GroupMicroTextureRegionsFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/provenance/GroupMicroTextureRegionsFilter.md @@ -1,104 +1,107 @@ -# Exemplar Archive Provenance: Inlined in Test +# Exemplar Archive Provenance: Inline fixtures -This sidecar records how the test data used by `GroupMicroTextureRegionsFilter`'s Class 1 / Class 4 unit tests was generated. It is the answer to "where did this hand-built data come from?" +This sidecar records the analytical inputs, independent expectations, release-source checks, and saved comparison evidence for `GroupMicroTextureRegionsFilter`. -The test data is **inlined** in the test source — there is no separate tar.gz archive, no `download_test_data()` entry, and no `.dream3d` exemplar file to fetch. The fixture scaffold is built in C++ via `AnalyticalFixtures::CreateScaffold(numFeatures)` and the canonical 5-feature fixture by `AnalyticalFixtures::Build5FeaturePureBunge()`. +## Archive identity ---- +| Field | Value | +|---|---| +| **Archive** | None. The oracle fixtures are constructed in the test source. | +| **SHA512** | Not applicable. No archive for this filter is registered in `src/Plugins/OrientationAnalysis/test/CMakeLists.txt`. | +| **Used by tests** | `Class 1 Analytical (Pure-Phi Bunge)`, `RandomizeParentIds invariants`, `Class 1 Analytical (Tolerance Boundary)`, `Class 1 Analytical (Running Average)`, `Class 1 Analytical (Mixed-Laue Rejection)`, `Non-contiguous neighbor grouping`, `Default contiguous-only neighbor mode`, and `Phase Index Bounds`. All use the `OrientationAnalysis::GroupMicroTextureRegionsFilter:` prefix. | +| **Generated by** | Michael Jackson (original analytical fixtures); Joey Kleingers (phase-bounds regression). | +| **Generated on** | 2026-06-11 (original fixture record); inputs are constructed on each test invocation. | +| **Generated at commit** | `a5da4e1f0` (PR #1637, original inline fixtures); `5a0b8327a` (PR #1703, expanded fixtures); `4ec21e93f` (PR #1568, phase-bounds regression). | -## Archive identity +The source is `src/Plugins/OrientationAnalysis/test/GroupMicroTextureRegionsTest.cpp`. It contains nine `TEST_CASE` definitions. Eight construct inputs inline; `SIMPL Backwards Compatibility` reads the two repository JSON fixtures under `test/simpl_conversion/6_4/` and `6_5/`. -| Field | Value | -|-------------------|----------------------------------------------------------------------------------------------------------------| -| **Archive** | Inlined (no separate archive) | -| **SHA512** | N/A | -| **Used by tests** | `OrientationAnalysis::GroupMicroTextureRegionsFilter: Class 1 Analytical (Pure-Phi Bunge)`, `…: RandomizeParentIds invariants`, `…: Class 1 Analytical (Tolerance Boundary)`, `…: Default contiguous-only neighbor mode` | -| **Generated by** | Michael Jackson (V&V cycle), inspired by the `ComputeFeatureNeighborCAxisMisalignmentsFilter` Class 1 scaffold pattern | -| **Generated on** | 2026-06-11 | -| **Generated at commit** | *Pending — V&V branch `vv/group_microtexture_regions`; commit hash filled in at SBIR deliverable assembly* | +All pass in CI at `419f9db35` on Linux, macOS, and Windows, using the supplied in-core CI evidence. ## How it was generated -The dataset is a hand-rolled minimal `{nX, 1, 1}` ImageGeom designed as a **Class 1 (Analytical) + Class 4 (Invariant) oracle** for the c-axis-tolerance grouping algorithm. Every quantity needed by the filter is set explicitly per feature so the expected groupings derive in closed form from the chosen tolerance and neighbor adjacency. +`AnalyticalFixtures::CreateScaffold()` constructs a synthetic image with one cell per real Feature and a background Feature at index zero. It supplies explicit quaternions, phases, unit volumes, and contiguous and non-contiguous neighbor lists. The five analytical fixtures use hand-derived expectations; no computed filter output supplies their expected groups. -1. **Geometry:** `ImageGeom` of dimensions `{nX, 1, 1}` with `nX = numFeatures - 1`. Spacing `{1, 1, 1}` and origin `{0, 0, 0}`. Each cell maps to a different real feature (`FeatureIds[k] = k + 1`), so the geometric layout is incidental — the filter only consumes per-feature data (quaternions, phases, volumes, neighbor list) and the cell→feature mapping for the final cell-parent backfill. The compact one-cell-per-feature layout exists purely so the test can assert the cell-feature consistency invariant on a small known mapping. +The image dimensions are `{numFeatures - 1, 1, 1}`, spacing is `{1, 1, 1}`, and origin is `{0, 0, 0}`. Geometry does not enter the grouping calculation. The cell-to-Feature mapping exists to check the final parent assignment. Three ensemble entries represent the background sentinel, hexagonal-high, and cubic-high classes. The background has phase zero and no cell assigned to it. All Features have unit volume; these fixtures do not distinguish unequal volume weights. -2. **Feature structure:** `numFeatures` total tuples (default 6 for the canonical fixture: background feature 0 + five real features F1..F5). Feature 0 is the background sentinel with `FeaturePhases[0] = 0`; it has no cell assigned to it but exists so `FeaturePhases`, `Volumes`, `AvgQuats`, and `NeighborList` all have a 0-th tuple for `featureId = 0` lookups. Real features get `FeaturePhases = 1`. All `Volumes` default to `1.0f` (uniform); only matters under `UseRunningAverage=true`, which the current fixtures do not exercise. +`QuatFromPhiDeg()` stores `{sin(Phi/2), 0, 0, cos(Phi/2)}` in x/y/z/w order. For these pure-Bunge rotations, the sample-frame c-axis is `(0, sin(Phi), cos(Phi))`. The c-axis angle is the difference between the selected Phi values, folded for antipodal equivalence. Current fixtures stay in one hemisphere, so they do not exercise antipodal acceptance. -3. **Ensemble structure:** 2 phase entries. `CrystalStructures[0] = 999u` (sentinel — phase 0 is never resolved against this index in well-formed inputs), `CrystalStructures[1] = ebsdlib::CrystalStructure::Hexagonal_High`. The filter only groups features whose phase resolves to Hexagonal_High, so this single Laue class is sufficient to cover the production code path. +| Fixture | Inputs and independent expectation | +|---|---| +| Pure-Phi Bunge | F1 through F5 have Phi values 0, 5, 60, 63, and 25 degrees. Edges connect F1–F2–F3–F4; F5 is isolated. At 10 degrees, expected groups are `{F1,F2}`, `{F3,F4}`, and `{F5}`. | +| RandomizeParentIds invariants | Three copies of the five-Feature fixture run with seed 42. One disables label randomization and two enable it. All preserve group membership and cell mapping; the two randomized outputs agree exactly. At least one real-Feature label must change relative to the unshuffled run. | +| Tolerance Boundary | A 0/8/20-degree chain at 10-degree tolerance gives `{F1,F2}` and `{F3}` with touching-Feature comparison. The two relevant separations are 8 and 12 degrees. This is not an exact floating-point boundary test. | +| Running Average | A 0/9/18-degree chain with unit volumes at 10-degree tolerance gives two groups. After one pair joins, its average is 4.5 or 13.5 degrees; the remaining end differs by 13.5 degrees. The test executes seed 42 and asserts the group count and membership constraints. | +| Mixed-Laue Rejection | Twenty isolated pairs each contain one cubic-high and one hexagonal-high Feature. Their c-axes align. Running-average mode must reject every pair because the reference and candidate must both be hexagonal-high. | +| Non-contiguous neighbor grouping | Two Features at 0 and 2 degrees have empty contiguous lists and mutual non-contiguous links. At 10 degrees, they must share a parent. | +| Default contiguous-only neighbor mode | Two Features at 0 and 2 degrees have mutual contiguous links. The test checks successful preflight and execution with optional neighbors disabled. It does not check the output parent values. | +| Phase Index Bounds | Three real Features include one touching pair and one isolated Feature. Separate sections assign an out-of-range positive phase to each touching Feature and expect `-87001`. An invalid phase on the isolated Feature permits execution. | -4. **Orientations (pure-Phi Bunge):** Each feature's average orientation is a pure Bunge ZXZ Euler rotation `(φ1 = 0, Φ = Φᵢ, φ2 = 0)`. With `φ1 = φ2 = 0`, the orientation matrix reduces to `R = R_x(Φᵢ)`, and the crystal-frame c-axis `[0, 0, 1]` projected into sample frame becomes exactly `(0, sin Φᵢ, cos Φᵢ)`. The closed-form angular distance between any two features' c-axes is therefore `|Φ_A − Φ_B|`, folded into `[0°, 90°]` by the algorithm's `w ≤ tol || (π − w) ≤ tol` acceptance check. The quaternion stored is `{x = sin(Φᵢ/2), y = 0, z = 0, w = cos(Φᵢ/2)}`, set via the helper `QuatFromPhiDeg(Φ_deg)`. This convention matches the sibling `ComputeFeatureNeighborCAxisMisalignmentsFilter` Class 1 oracle. +The unused `BuildLargeBenchmarkFixture()` helper does not define an executing test case and does not supply full-batch coverage. -5. **Neighbor list:** `NeighborList` of `numFeatures` tuples, each set explicitly via `SetNeighbors(td, featureId, {…})`. Empty by default; callers set the adjacency for the specific scenario being tested. +Release exposure was checked directly in all ten `upstream/release/v` trees. No path containing `GroupMicroTextureRegions` exists in v7.0.0, v7.0.1, v7.0.2, v7.0.3, v7.2.0, v7.2.1, v7.2.2, v7.4.0, or v7.4.1. The v7.4.2 filter and algorithm are under `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/`. -6. **Canonical 5-feature pure-Phi fixture (`Build5FeaturePureBunge`):** - - Per-feature Φ (degrees): `F1=0, F2=5, F3=60, F4=63, F5=25`. - - Contiguous neighbor list: `F1↔F2, F2↔F3, F3↔F4`, F5 isolated. - - Under 10° tolerance: `|0−5|=5 ≤ 10` (F1↔F2 group), `|5−60|=55 > 10` (F2↔F3 no bridge), `|60−63|=3 ≤ 10` (F3↔F4 group), F5 has no neighbors (alone). **Expected: 3 distinct groups → {F1,F2}, {F3,F4}, {F5}.** - - Re-used by the `RandomizeParentIds invariants` test, which runs the filter three times on independent copies of the fixture to assert equivalence-class preservation and same-seed determinism. +| ID | Release-source result | +|---|---| +| D3 | `Algorithms/GroupMicroTextureRegions.cpp` in v7.4.2 reads both Laue classes unconditionally. No DREAM3D-NX release has the one-sided check. | +| D5 | `GroupMicroTextureRegionsFilter.cpp` in v7.4.2 has no `Active` parameter or output action. | +| D6 | The v7.4.2 filter enables `UseSeed` by default. PR #1703 (`5a0b8327a`) contains the false-to-true change. No DREAM3D-NX release has the defective default. | +| D8 | The v7.4.2 algorithm reads Crystal Structures through unchecked positive phase indices. PR #1568 (`4ec21e93f`) adds the reference and neighbor bounds checks. The fix will be in the DREAM3D-NX release after v7.4.2. | -7. **Tolerance boundary fixture (inlined in the Tolerance Boundary test):** - - Per-feature Φ: `F1=0, F2=8, F3=20`. Chain neighbors: `F1↔F2↔F3`. - - Under 10° tolerance: `|0−8|=8 ≤ 10` (F1↔F2 group), `|8−20|=12 > 10` (F2↔F3 no bridge, using F2's c-axis vs F3's under `UseRunningAverage=false`). **Expected: 2 distinct groups → {F1,F2}, {F3}.** - - Exercises on-the-boundary acceptance (8° just inside) vs rejection (12° just outside). +The DREAM.3D `v6.5.171` tag contains `Source/Plugins/Reconstruction/ReconstructionFilters/GroupMicroTextureRegions.cpp` and the `GroupFeatures.cpp` base class. `SourceList.cmake` puts this filter in `_PrivateFilters` and passes `FALSE` to `ADD_SIMPL_FILTER`. The private source has the one-sided Laue check (D3), clock-derived seeds (D6), and unchecked phase indices (D8). Its constructor sets running average to false, tolerance to 1 degree, and parent randomization to true. Its `dataCheck()` creates `Active` with true values. -8. **Default-mode fixture (inlined in the default-mode test):** A trivial 2-feature scaffold (F1=0°, F2=2°, mutual neighbors). Whether grouping occurs is incidental — the test exists to confirm the filter runs cleanly in the default `UseNonContiguousNeighbors=false` (contiguous-only) mode. +The tag date is 2026-05-27. The last SIMPL snapshot on or before that date is `0afaa6854ac0ef27df17278e11be711f74f77b90`. `Source/SIMPLib/Math/SIMPLibMath.h` defines `SIMPLib::Constants::k_Pi` as double. The tagged filter multiplies the tolerance by that constant before storing a float threshold; current SIMPLNX uses `k_PiF` (D7). -9. **Quaternion convention verification:** The `QuatFromPhiDeg` helper produces `{sin(Φ/2), 0, 0, cos(Φ/2)}` (storage order `{x, y, z, w}`), matching what `ebsdlib::Quaternion(q[0], q[1], q[2], q[3])` consumes in the algorithm at lines 257 and 305 of `Algorithms/GroupMicroTextureRegions.cpp`. The convention is sibling-shared with `ComputeFeatureNeighborCAxisMisalignmentsFilter`, whose Class 1 oracle was reviewed previously. +## Canonical oracle output -The dataset definition lives in `src/Plugins/OrientationAnalysis/test/GroupMicroTextureRegionsTest.cpp` namespace `AnalyticalFixtures` (functions `CreateScaffold`, `Build5FeaturePureBunge`, helpers `QuatFromPhiDeg`, `SetAvgQuat`, `SetNeighbors`, `BuildArgs`). +| DataPath | Source of expected values | +|---|---| +| `/ImageGeometry/CellFeatureData/FeatureParentIds` | Class 1 group membership derived from angles, phases, and adjacency. Assertions compare same-group and different-group relations rather than absolute labels. | +| `/ImageGeometry/CellData/CellParentIds` | Class 4: each cell parent equals `FeatureParentIds[FeatureIds[cell]]`. | +| `/ImageGeometry/MicroTextureFeatureData` | Class 4: tuple count equals the maximum Feature-parent ID plus one for index zero. | +| `/GroupMicroTextureRegions_Seed` | Class 4: the stored seed equals the requested value, 42. This is the name selected by the fixture. | -## Canonical oracle output +The `Pure-Phi Bunge` test also checks the absence of `/ImageGeometry/MicroTextureFeatureData/Active`. Randomization preserves pairwise group membership, group count, positive real-Feature labels, and cell-to-Feature consistency. None of these expected values comes from an exemplar generated by the filter. -| DataPath | Source of expected values | -|------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `/ImageGeometry/CellFeatureData/FeatureParentIds` | Class 1 (Analytical) — per-pair grouping outcome derived in closed form from `\|Φ_A − Φ_B\|` and the contiguous neighbor adjacency. Encoded as same-group / different-group pairwise checks rather than absolute parent-id values, to remain invariant under deterministic seed-iteration order. | -| `/ImageGeometry/CellData/CellParentIds` | Class 4 (Invariant) — must equal `FeatureParentIds[FeatureIds[k]]` for every cell `k`. Encoded as a loop check; no independent oracle derivation is needed because the value is determined by the feature-parent assignment plus the cell→feature mapping. | -| `/ImageGeometry/MicroTextureFeatureData` (`AttributeMatrix` tuple count) | Class 4 (Invariant) — must equal `max(FeatureParentIds) + 1` (index 0 reserved for unassigned). | -| `/GroupMicroTextureRegions_Seed` (top-level `UInt64Array`) | Class 4 (Invariant) — must equal the user-supplied seed value when `UseSeed=true`. Pins the seed-roundtrip behaviour. | +## Oracle provenance (Classes 2, 3, 5 only) -For the `RandomizeParentIds invariants` test the canonical oracle output is the *set of invariants randomization must preserve* (equivalence classes, group count, cell/feature consistency, positivity, same-seed determinism) rather than specific array values. See the V&V report's Code-path coverage row #9 and the test's inline comments for the invariant list. +Not applicable to the correctness oracle. Classes 1 and 4 live in the test source. The following local proof comparison supplies separate migration evidence. -The V&V branch retires the unused `/ImageGeometry/MicroTextureFeatureData/Active` output. The filter never consumed its values, and the SIMPLNX port never initialized them to the legacy `true` value. See `GroupMicroTextureRegionsFilter-D5`. +`vv_work:GroupMicroTextureRegionsFilter/ReadMe.md` dates the comparison to 2026-08-27. The runnable legacy implementation was a local proof version. DREAM.3D v6.5.171 could not instantiate this private filter. The production input came from `915C_50red Data.h5oina`: 2,326,310 cells, 1,840,838 Feature tuples, and 3,710,475 undirected touching pairs. `vv_work:GroupMicroTextureRegionsFilter/to_legacy.py` records the format conversion. The filter inputs were identical in the two file formats. -## Oracle provenance (Classes 2, 3, 5 only) +The comparison used a 20-degree tolerance and disabled non-contiguous neighbors. The final proof patch used one seeded generator, checked both Laue classes, used float32 tolerance conversion, and disabled the final parent-ID shuffle. The patch is `vv_work:GroupMicroTextureRegionsFilter/notes/legacy_exact_parity.patch`. -N/A — Class 1 (Analytical) + Class 4 (Invariant) oracle. Both lower-drift classes have their derivation embedded in the test source (the `QuatFromPhiDeg` comment for the c-axis math; the per-test comment block at the top of each TEST_CASE for the expected groupings and the invariants). +| Case | Matching Feature-parent IDs | Matching cell-parent IDs | Groups | +|---|---|---|---| +| Seed 5489, running average disabled | 1,840,838 of 1,840,838 | 2,326,310 of 2,326,310 | 746,877 | +| Seed 5489, running average enabled | 1,840,838 of 1,840,838 | 2,326,310 of 2,326,310 | 795,323 | +| Seed 1234, running average enabled | 1,840,838 of 1,840,838 | 2,326,310 of 2,326,310 | 795,044 | -## Second-engineer oracle review +All three cases have zero differing partitions and zero differing touching-pair decisions. `Active` is present only in the local proof output. Source: `vv_work:GroupMicroTextureRegionsFilter/rerun/final_exact_parity_results.txt`. -- **Reviewer:** **Adam L. Pilchak, Pratt & Whitney** — external domain expert. Review request `group_microtexture_regions_review_ALP.pdf` sent 2026-08-11, signed and returned 2026-08-11. Scope was explicitly *metallurgical intent only*; the reviewer records that he "did not review software implementation, source code, numerical accuracy, or test code." +The initial comparison without proof corrections matched partitions when running average was disabled, but not raw labels. With running average enabled, it had 198,626 differing touching-pair decisions, including 923 cross-Laue pairs. The first proof correction removed cross-Laue disagreements but left two hexagonal/hexagonal differences at seed 5489. Changing only tolerance conversion removed those two differences; changing vector-normalization precision did not. Sources: `vv_work:GroupMicroTextureRegionsFilter/rerun/comparison_results.txt`, `vv_work:GroupMicroTextureRegionsFilter/rerun/pif_hypothesis_results.txt`, and `vv_work:GroupMicroTextureRegionsFilter/rerun/floatnorm_hypothesis_results.txt`. - **Outcome by scenario:** +Separate preserved experiments had 194,658 differing pairs between repeated executions and 192,496 between selected seeds. All were hexagonal/hexagonal, with no cross-Laue differences. These counts describe those runs, not every seed choice. Source: `vv_work:GroupMicroTextureRegionsFilter/rerun/preserved_seed_variation_results.txt`. - | Scenario | Question | Reviewer's answer | Effect on the oracle | - |---|---|---|---| - | A — chaining | Do three grains at 0°/8°/16° in a touching chain form one region at 10° tolerance? | *"It depends"* — on the comparison target. Pixel-to-pixel → one region {A,B,C}; running average → {A,B} and {C}. | **Resolves open item (a).** Chain-transitive closure is the correct, intended semantic *for the neighbour-to-neighbour comparison target*, and the running-average option is the intended remedy for unbounded drift. Not an inherited accident. | - | B — hard cutoff | Where does the region end at 8° / 12° with a 10° tolerance? | **{D,E} and {F}** — agrees with the implementation. Adds that 10° is *"too small a value for MTR segmentation"* and that he would typically use **20°**, and asks whether alternatives to a hard cutoff exist. | Confirms the tolerance semantics. **Action:** document a recommended starting tolerance (~20°) in the filter docs; a soft/hysteresis cutoff is an open enhancement question, not a defect. | - | C — comparison target | Neighbour grain, or the region's running volume-weighted average? | *"Both should be user-defined options"* — neighbour-to-neighbour for misorientation-constrained analyses, running average for ensemble averaging feeding structure–property modelling. Volume weighting *"makes sense to me."* He states a personal preference for the running average. | Confirms the current design (both offered, `UseRunningAverage` flag) and endorses volume weighting. | - | D — phase boundary | Should two touching grains of *different phases* that share hexagonal symmetry group together? | **Yes** — *"assuming this is a case of primary alpha and transformed beta."* | Confirms that keying the acceptance test on **Laue class** rather than phase identity is correct. Note this validates same-Laue-class cross-phase grouping only; it does **not** bless cross-*Laue-class* grouping, which is the residual legacy defect in D3. | +The production comparison used SIMPLNX `42287fe556f7b696ffd93e1d024d819a9aba02b7`; the final V&V source is recorded as `c89e05cb9524d92e9f77681cb897b8653b18781d`. Two later commits change the algorithm source: `4ec21e93f` (2026-09-20, PR #1568: buffered Feature data, cell mapping, and phase bounds) and `a33c370ca` (2026-09-25, PR #1697: messaging API). The 2026-08-27 comparison therefore does not establish parity for the current algorithm. - **Erratum owed to the reviewer.** The Scenario C briefing told him that in original DREAM3D the running-average option *"never actually worked — a defect meant no grains ever grouped when it was switched on."* He replied: *"We have been using this option successfully for many years and always group plenty of grains that match the eyeball metallurgy test when compared to IPF maps."* **The reviewer is correct; that briefing statement was wrong and must be retracted.** The legacy defect weakens the Laue-class check rather than breaking it, so it can over-group but never fail to group — see deviation D3 for the mechanism and the measured effect. A short correction should be sent to him. +Test sensitivity verification: `vv_work:GroupMicroTextureRegionsFilter/rerun/mutation_d3_red.log` records 14 failed pair assertions after the one-sided Laue condition was restored temporarily. This demonstrates sensitivity to D3; it is not an exhaustive mutation campaign. - **Additional request (out of scope for the oracle, tracked for the product):** *"This survey does not comment on the contiguous versus non-contiguous nature of the filter… we DO want that feature to be enabled in NX."* The `UseNonContiguousNeighbors` option exists in the SIMPLNX filter; defect D1 had made the *default* (`false`) path unusable and is now fixed. Confirm the non-contiguous path itself is exercised and documented before replying to him. +## Second-engineer oracle review - **Prior note retained for history:** the pure-Phi Bunge derivation is sibling-shared with `ComputeFeatureNeighborCAxisMisalignmentsFilter` (already reviewed). +- **Reviewer:** Joey Kleingers. +- **Date:** 2026-09-20, as recorded in the report sign-off. - **Resolved by source inspection during the 2026-08-10 gate-closing pass (V&V author; still needs independent confirmation):** item (a) below — the `UseRunningAverage=false` - comparison target — is confirmed correct as stated in this sidecar. `execute()` iterates `for(j = 0; j < groupList.size(); j++)` over a list that grows as neighbors are - accepted and passes `groupList[j]` to `determineGrouping()` as `referenceFeature`; the local is misleadingly named `firstFeature` but is the current BFS frontier feature, so - each candidate is compared against its immediately-preceding feature, not the seed and not a running average. The Tolerance Boundary fixture was additionally re-derived under - **all three possible seed orders** (`getSeed()` draws randomly among unparented features): seeding at F1, F2, or F3 each yields the partition {F1,F2}, {F3}, so the assertion - is seed-order independent. Note the report body previously described this semantic incorrectly and the test comment was self-contradictory; both were corrected to match this - sidecar, which was right. +Adam Pilchak reviewed metallurgical intent on 2026-08-11. The signed record is `vv_work:GroupMicroTextureRegionsFilter/notes/group_microtexture_regions_review_ALP.pdf`. It explicitly excludes software implementation, source code, numerical accuracy, and test code from his review. - **Item (a) — CLOSED by the Pilchak review (Scenario A).** The transitive-closure semantic (features at Φ = 0°, 8°, 16° all merging at 10° tolerance) is confirmed as the intended user-facing behaviour for the neighbour-to-neighbour comparison target, not an inherited accident. The running-average target is the intended way to bound the drift. +| Scenario | Review result | +|---|---| +| Neighbor-chain growth | The selected comparison target determines whether the entire chain joins. Both targets should be available. | +| Hard cutoff | An 8-degree pair joins at a 10-degree tolerance; a 12-degree pair does not. The reviewer uses 20 degrees in his own MTR work. | +| Comparison target and weights | Touching-Feature and running-average modes serve different analyses. Volume weighting is appropriate for the stated use. | +| Different phases with shared symmetry | Grouping is appropriate for the stated primary-alpha and transformed-beta example. This does not justify grouping different Laue classes. | - **Still open:** - (b) Whether the invariant list in `RandomizeParentIds invariants` is exhaustive — that no observable behaviour of the shuffle is left unasserted. *This is a software-test question and is explicitly outside the scope of the Pilchak review; it still needs a BlueQuartz second engineer.* -- **Date:** 2026-08-11 (metallurgical oracle review, A. Pilchak); software-side second-engineer review still pending. -- **Skip reason** (if skipped): *N/A.* +The review request incorrectly stated that the legacy running-average option never grouped grains. The reviewer reported successful use, and the source confirms that D3 weakens the Laue check rather than preventing grouping. The review also requests non-contiguous neighbor support; `Non-contiguous neighbor grouping` now asserts that path's output. ## Regenerated to fix a circular-oracle situation? -N/A. This dataset is brand-new for the SIMPLNX V&V cycle; no prior exemplar existed for this filter that needed retroactive replacement. The pre-V&V test was tagged `[.][UNIMPLEMENTED][!mayfail]` with empty `DataPath{}` arguments throughout and never actually exercised the algorithm; it was retired in favour of the inlined Class 1 + Class 4 fixtures documented here. +No. There is no exemplar archive to regenerate. The inline analytical expectations replace an empty-path placeholder test. The production comparison outputs explain migration differences and do not define the correctness oracle. diff --git a/src/Plugins/OrientationAnalysis/vv/provenance/NeighborOrientationCorrelationFilter_inputs.md b/src/Plugins/OrientationAnalysis/vv/provenance/NeighborOrientationCorrelationFilter_inputs.md new file mode 100644 index 0000000000..1b87636d47 --- /dev/null +++ b/src/Plugins/OrientationAnalysis/vv/provenance/NeighborOrientationCorrelationFilter_inputs.md @@ -0,0 +1,39 @@ +# Exemplar Archive Provenance: Small_IN100_dream3d_v3.tar.gz + +## Archive identity + +| Field | Value | +|---|---| +| **Archive** | `Small_IN100_dream3d_v3.tar.gz` | +| **SHA512** | `bfa9547e787b0f8e8122702da0eb4e519f48a48bf4f94aad020f72479d071d32dfc96a1425705874c68507a61ed391d28606d9c4f4acd559043ef0ace64fd33f` | +| **Used by tests** | `OrientationAnalysis::NeighborOrientationCorrelationFilter: Small IN100 Pipeline` | +| **Generated by** | Joey Kleingers (inferred from PR #1328) | +| **Generated on** | 2025-06-13 to 2025-06-20 (inferred from PR #1328) | +| **Generated at commit** | `b931e70d4` (PR #1328 merge, inferred) | + +## How it was generated + +PR #1328 first registers this archive in `src/Plugins/OrientationAnalysis/test/CMakeLists.txt` and replaces the v2 registration. The archive supplies `Small_IN100.dream3d` as input to the six preparation filters in `src/Plugins/OrientationAnalysis/test/NeighborOrientationCorrelationTest.cpp::Small IN100 Pipeline`. The test snapshots the resulting cell arrays before it executes Neighbor Orientation Correlation. + +## Canonical oracle output + +The archive supplies input only. It supplies no expected Neighbor Orientation Correlation output. + +| DataPath | Source of expected values | +|---|---| +| `DataContainer/CellData/*` (numeric arrays) | Class 4: compare with the pre-filter snapshots; every changed cell must have had low confidence, and at least one cell must change. | + +## Oracle provenance (Classes 2, 3, 5 only) + +Not applicable to this archive. The production test uses Class 4 invariants. Analytical fixtures and the NumPy reference are described in `neighbor_orientation_correlation_v2.md`. + +## Second-engineer oracle review + +- **Reviewer:** Michael Jackson (technical authority) +- **Date:** 2026-07-16 + +This is the existing V&V sign-off. It names no separate second engineer. + +## Regenerated to fix a circular-oracle situation? + +No. This archive is an input fixture, not a replacement output oracle. The production test no longer reads either retired `neighbor_orientation_correlation` output archive. The retirement record is `neighbor_orientation_correlation_v2.md`. diff --git a/src/Plugins/OrientationAnalysis/vv/provenance/ReadAngDataFilter.md b/src/Plugins/OrientationAnalysis/vv/provenance/ReadAngDataFilter.md index d0360afe27..ddfd2129ab 100644 --- a/src/Plugins/OrientationAnalysis/vv/provenance/ReadAngDataFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/provenance/ReadAngDataFilter.md @@ -1,40 +1,76 @@ -# Exemplar Archive Provenance: read_ang_test.tar.gz — RETIRED +# Exemplar Archive Provenance: read_ang_test.tar.gz -This sidecar documents the **retirement** of `read_ang_test.tar.gz` during the ReadAngDataFilter V&V pass (2026-07-07). The archive is no longer referenced by any test or `download_test_data()` entry. +This sidecar records the retired archive, its inline replacement and the separate legacy comparison evidence. ---- - -## Archive identity (at retirement) +## Archive identity | Field | Value | |---|---| -| **Archive** | `read_ang_test.tar.gz` | +| **Archive** | `read_ang_test.tar.gz` (retired; no current test consumer) | | **SHA512** | `de7cd89d925da01f291f44686964ec89d469659d0005219f9869afe26b8f62af278461ac3f5deb3afe7f3e65ec074ab3a1357d77a1a5f92eb3a1ea8cc5e4b236` | -| **Was used by tests** | `OrientationAnalysis::ReadAngData: Exemplary Test`, `…: Invalid Phase`, `…: Invalid Columns & Rows` (all retired/replaced) | -| **Retired by** | Michael Jackson | -| **Retired on** | 2026-07-07 | +| **Used by tests** | None now; formerly `OrientationAnalysis::ReadAngData: Exemplary Test`, `OrientationAnalysis::ReadAngData: Invalid Phase` and `OrientationAnalysis::ReadAngData: Invalid Columns & Rows`. | +| **Generated by** | Joey Kleingers (inferred from PR #1462) | +| **Generated on** | 2025-10-28 to 2025-11-11 (inferred from PR #1462) | +| **Generated at commit** | `2729ec1e0` (PR #1462 merge, inferred) | -## Why it was retired +The first registration of this archive in `src/Plugins/OrientationAnalysis/test/CMakeLists.txt` is `2729ec1e0`, and its SHA512 matches the value above. PR #1657 (`49ea8b09c`) removes that registration and the three archive-based tests. The current CMake file has no entry for this archive. -**Circular oracle.** The archive bundled `read_ang_test.dream3d`, an exemplar generated by running this same filter and saving its output — so the "Exemplary Test" verified only that the filter still produced whatever it produced when the exemplar was made, not that the output was correct. Per the V&V policy (and the standing project rule that exemplar archives be retired in favor of inline toy data), it was replaced rather than regenerated. +## How it was generated -## Replacement +PR #1462 states that the test data was generated from SIMPLNX output. Its test reads `read_ang_test.ang` and compares cell arrays with `read_ang_test.dream3d`; two further ANG files supply invalid-phase and truncated-data inputs. The current replacement constructs synthetic ANG text in `src/Plugins/OrientationAnalysis/test/ReadAngDataTest.cpp` and derives expected values directly from that text. -The oracle is now **inline in `test/ReadAngDataTest.cpp`** (no archive at all): +The base fixture has dimensions `(3, 2, 1)`, spacing `(0.25, 0.5, 1)`, and cubic Nickel and hexagonal Titanium phase sections. Its Euler components and cell columns distinguish axes and components. The origin is zero because the filter fixes it at zero. Most cell values are exactly representable in float32; lattice constants such as 3.52, 2.95 and 4.68 use the corresponding float32 literals, not exact binary fractions. A phase-2-only header checks unused ensemble slots. Separate header and data variants check five errors. -- A hand-authored toy `.ang` fixture (string literal, written to the binary test-output dir at runtime) with **Class 1 hand-derived expected values** for every output array and the geometry, plus **Class 4 invariants** (ensemble sizing, Invalid-Phase slot defaults). -- The archive's two invalid `.ang` error fixtures are replaced by inline variants (no-phase header → `-150`; truncated data → `-600`), plus new value-add error fixtures (HexGrid → `-19500`, missing GRID → `-19501`) and a non-contiguous-phase fixture. +The independent oracle is in the test source. The following runtime comparisons are separate migration evidence: -See the report's Oracle section: `src/Plugins/OrientationAnalysis/vv/ReadAngDataFilter.md`. +| Run date | Application and inputs | Recorded result | Artifact | +|---|---|---|---| +| 2026-07-07 | DREAM.3D v6.5.171 and SIMPLNX; toy ANG and Small IN100 `Slice_1.ang` | 2 of 2 supported-input cases match in all numeric arrays and geometry values; material names differ by trailing spaces. | `vv_work:ReadAngDataFilter/ComparisonSummary.md` | +| 2026-07-07 | DREAM.3D v6.5.171; sparse phase-2 toy | Crash, recorded exit code 139. | `vv_work:ReadAngDataFilter/ComparisonSummary.md`; `vv_work:ReadAngDataFilter/legacy_sparse_run.log` | +| 2026-09-17 | A local proof version with corrected ensemble sizing and defaults | Sparse input imports and its numeric datasets match the saved SIMPLNX output; both supported inputs reproduce all 14 datasets of their stock legacy baselines. | `vv_work:ReadAngDataFilter/ReadMe.md`; `vv_work:ReadAngDataFilter/LegacyPatch.md` | -## Second-engineer oracle review +`vv_work:ReadAngDataFilter/ComparisonSummary.md` identifies the stock application runner. `vv_work:ReadAngDataFilter/legacy_sparse_run.log` has the `SIMPLib Version 1.2.828.f45085c83` banner. The proof uses `vv_work:ReadAngDataFilter/run_sparse_proof.py` and `vv_work:ReadAngDataFilter/run_wellformed_proof.py`; their comparisons use `numpy.array_equal`. The sparse comparison excludes MaterialName. The proof stdout files record successful completion for sparse, toy and Small IN100 inputs. The patch changes ensemble allocation and default initialization only. + +The algorithm source changed after the comparison dates. The current report therefore does not claim that those results establish current parity. `git log --since=2026-07-07` on `Algorithms/ReadAngData.cpp` and `Algorithms/ReadAngData.hpp` gives: + +| Date | Commit | Change | +|---|---|---| +| 2026-07-23 | `49ea8b09c` (PR #1657) | Adds phase-index and reader-count guards, sparse ensemble allocation, full default initialization and progress messages. | +| 2026-07-24 | `1d02339d5` | Changes the input-path declaration in `ReadAngData.hpp` to the parameter value-type alias. | +| 2026-09-20 | `4ec21e93f` (PR #1568) | Changes cell copies to bulk store writes and Euler interleaving to bounded blocks. | +| 2026-09-25 | `a33c370ca` (PR #1697) | Updates the messaging API. | + +The last two changes also follow the 2026-09-17 proof run. + +Release checks read `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ReadAngDataFilter.cpp` and `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ReadAngData.cpp` at every `upstream/release/v` from v7.0.0 through v7.4.2. Releases through v7.4.1 allocate `phaseCount + 1` tuples, write phase indices without bounds checks and copy reader buffers without a count guard. The v7.4.2 sources contain all three guards and `max(maxPhaseIndex, phaseCount) + 1` allocation. They establish that D3 and D6 are fixed in DREAM3D-NX v7.4.2. + +DREAM.3D v6.5.171 was affected by D3 and D6. Its tagged `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/ReadAngData.cpp` uses count-based ensemble allocation and unchecked indexed writes and buffer copies. Its tagged `Source/EbsdLib/TSL/AngReader.cpp` uses even columns for geometry and positive odd columns first for square-grid buffer allocation. These sources also confirm the material-name, unit, error-code and phase-0 differences D1, D2, D4 and D5. -Skipped — documented reason: the filter's value-add is deterministic data plumbing (grid math from header values, verbatim column copies, a documented TSL-symmetry enum mapping, string trim). Every expected value is mechanically derivable from the fixture text with no design freedom, so the author-bias failure mode the second-engineer review guards against has no room to operate. The oracle fixture and derivations are fully visible in one screen of test source for any reviewer to audit at sign-off. +## Canonical oracle output -## Circular-oracle note +The retired archive is not a correctness oracle. These paths identify the current inline expectations under `DataContainer`. + +| DataPath | Source of expected values | +|---|---| +| `/DataContainer` | Class 1 header dimensions and spacing, zero origin and Micrometer units. | +| `/DataContainer/CellData/Phases` | Class 1 remap of `{1,2,0,1,2,0}` to `{1,2,1,1,2,1}`. | +| `/DataContainer/CellData/EulerAngles` | Class 1 interleave of the three written angle columns, in file order. | +| `/DataContainer/CellData/{Image Quality,Confidence Index,SEM Signal,Fit,X Position,Y Position}` | Class 1 literal values copied from the fixture columns. | +| `/DataContainer/CellEnsembleData/CrystalStructures` | Class 1 symmetry mapping: `{999,1,0}` for the two-phase fixture and `{999,999,0}` for the sparse fixture. | +| `/DataContainer/CellEnsembleData/MaterialName` | Class 1 trimmed names and Class 4 `Invalid Phase` defaults for unused slots. | +| `/DataContainer/CellEnsembleData/LatticeConstants` | Class 1 header constants and Class 4 zero defaults for unused slots. | + +The eight ordinary test cases cover these expectations, the five errors and SIMPL conversion. All pass in CI at `419f9db35` on Linux, macOS and Windows, using in-core builds. The current filter, algorithm and test file match that revision. The hidden block-boundary case defines a 65,537-point fixture and checks the final point of a full block and its one-point tail. + +## Oracle provenance (Classes 2, 3, 5 only) + +Not applicable. Classes 1 and 4 define correctness. EbsdLib supplies parsing and parser diagnostics; the test does not use its output as a separate reference oracle. + +## Second-engineer oracle review -This retirement **removes** a circular oracle. The replacement was derived by hand from the fixture text before any DREAM3D (legacy or NX) run — verified order: oracle first (2026-07-06), SIMPLNX reconciliation (zero discrepancies), then the 6.5.171 comparison (2026-07-07, bit-identical numerics). +- **Reviewer:** skipped +- **Skip reason:** The original record treats the expected values as direct derivations from header values, column copies and symmetry mappings. The report has no named second engineer. -## Archive disposition +## Regenerated to fix a circular-oracle situation? -The tar.gz remains in the GitHub [Data_Archive release](https://github.com/BlueQuartzSoftware/simplnx/releases/tag/Data_Archive) for historical checkouts of older commits; it must not be referenced by new code. +The archive was retired, not regenerated. PR #1462 describes SIMPLNX-generated reference output; PR #1657 replaces that self-comparison with inline analytical and invariant fixtures. The removed tests checked one exemplar and the `-150` and `-600` errors; both error checks remain in the current inline tests. diff --git a/src/Plugins/OrientationAnalysis/vv/provenance/ReadCtfDataFilter.md b/src/Plugins/OrientationAnalysis/vv/provenance/ReadCtfDataFilter.md index 8f4639cae5..7dcb00294d 100644 --- a/src/Plugins/OrientationAnalysis/vv/provenance/ReadCtfDataFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/provenance/ReadCtfDataFilter.md @@ -1,45 +1,65 @@ -# Exemplar Archive Provenance: 6_6_read_ctf_data_2.tar.gz — RETIRED +# Exemplar Archive Provenance: 6_6_read_ctf_data_2.tar.gz -This sidecar documents the **retirement** of `6_6_read_ctf_data_2.tar.gz` during the ReadCtfDataFilter V&V pass (2026-07-24). The archive is no longer referenced by any test or `download_test_data()` entry. +This sidecar records the retired archive, its inline replacement, and the comparison evidence for `ReadCtfDataFilter`. ---- - -## Archive identity (at retirement) +## Archive identity | Field | Value | |---|---| -| **Archive** | `6_6_read_ctf_data_2.tar.gz` | +| **Archive** | `6_6_read_ctf_data_2.tar.gz` (retired 2026-07-24) | | **SHA512** | `f397fa3bf457615a90a4b48eaafded2aa4952b41ccb28d9da6a83adc38aea9c22f2bb5a955f251edeca9ef8265b6bf1d74e829b1340f45cf52620a237aad1707` | -| **Contents** | `6_6_read_ctf_data.dream3d` + `.xdmf` (legacy-generated exemplar), `Cugrid_after 2nd_15kv_2kx_2.ctf` (550×400 production scan, Cu3Ga hexagonal + Copper cubic) | -| **Was used by tests** | `OrientationAnalysis::ReadCtfData: Valid Execution` (retired/replaced) | -| **Retired by** | Michael Jackson | -| **Retired on** | 2026-07-24 | +| **Used by tests** | None currently. Formerly `OrientationAnalysis::ReadCtfData: Valid Execution`. | +| **Generated by** | Michael Jackson (inferred from PR #937) | +| **Generated on** | 2024-04-30 to 2024-05-01 (inferred from PR #937) | +| **Generated at commit** | `9d7c4e459` (PR #937 merge, inferred) | -## Why it was retired +`src/Plugins/OrientationAnalysis/test/CMakeLists.txt` first registered this name and SHA512 at `9d7c4e459`. That change replaced `6_6_read_ctf_data.tar.gz` when phase-zero preservation changed. The registration was removed at `1d02339d5`; the current CMake file has no ReadCtfData archive entry. The SHA512 above matches the registration at `9d7c4e459`. -**Legacy/circular oracle.** The archive's exemplar `.dream3d` was generated by a legacy DREAM3D 6.6 run of this same import path (`6_6_` prefix marks legacy-generated data), so the "Valid Execution" test verified only agreement with a prior DREAM3D output — exactly the "legacy produced this output" pattern the V&V policy forbids as a correctness oracle. Per the standing project rule, it was replaced with inline toy data rather than regenerated. +## How it was generated -## Replacement +The former test loaded `6_6_read_ctf_data.dream3d` and imported `Cugrid_after 2nd_15kv_2kx_2.ctf` from the archive. It compared the imported arrays with the stored arrays. The current fixtures are hand-authored CTF strings in `src/Plugins/OrientationAnalysis/test/ReadCtfDataTest.cpp`; `MakeToyCtf()` assembles them and `WriteCtfFile()` writes the inputs. They include a 3×2 two-phase grid, a three-phase conversion grid, and a 2×2×2 multi-slice grid. -The oracle is now **inline in `test/ReadCtfDataTest.cpp`** (no archive at all): +## Canonical oracle output -- A hand-authored toy `.ctf` fixture (string literal, written to the binary test-output dir at runtime) with **Class 1 hand-derived expected values** for the geometry and all 12 output arrays, plus **Class 4 invariants** (ensemble sizing, Invalid-Phase slot defaults). -- A 2×2 parameter sweep over DegreesToRadians × EdaxHexagonalAlignment whose expected Euler values are the correctly-rounded float32 results of double-precision arithmetic, computed independently with NumPy IEEE-754 float32/float64 semantics from the fixture text (constant: `M_PI/180.0`; derivation commented beside the literals). Four fixture angles (7.125°, 3.375°, 6.75°, and 37.125° via the hex shift) were deliberately chosen where the float32 and double-precision roundings **differ**, so the assertions pin the required double-precision code path (added after the adversarial review showed the original angles were blind to it). -- A 2×2×2 multi-slice fixture for the 3D `.ctf` support restored. -- Eight error fixtures: value-add guards (`-19600`…`-19604`; `-19605` is reachable only by file-mutation injection and is documented untested) and EbsdLib passthroughs (`-102`, `-105`, `-107`). +No archived output is used as a correctness oracle. The paths below refer to the inline fixtures; expected values are literals in the test source. -See the report's Oracle section: `src/Plugins/OrientationAnalysis/vv/ReadCtfDataFilter.md`. +| DataPath | Source of expected values | +|---|---| +| `/DataContainer/CellData/Phases` | Class 1: copy the input phase values, including zero. | +| `/DataContainer/CellData/EulerAngles` | Class 1: interleave the three input columns; apply the selected hexagonal shift and radian conversion with double intermediates before rounding to float32. | +| `/DataContainer/CellData/{Bands, Error, MAD, BC, BS, X, Y}` | Class 1: copy each input column without changing values. | +| `/DataContainer/CellEnsembleData/{CrystalStructures, MaterialName, LatticeConstants}` | Classes 1 and 4: derive phase metadata from the input header; reserve slot zero for unknown structure, Invalid Phase, and zero lattice constants. | -The archive's production scan (`Cugrid_after 2nd_15kv_2kx_2.ctf`) remains valuable as an **A/B comparison fixture** (not an oracle); it is used in the OneDrive-archived comparison working folder, not by any unit test. +The 12 default test cases run on Linux, macOS, and Windows. All pass in CI at `419f9db35`. The conversion fixture asserts all 18 Euler values for each of four modes and includes both hexagonal Laue classes. Values 7.125, 3.375, 6.75, and shifted 37.125 degrees distinguish float32 arithmetic from double-intermediate conversion. -## Second-engineer oracle review +The stock comparison ran on 2026-07-24. `vv_work:ReadCtfDataFilter/ReadMe.md` identifies the official DREAM.3D v6.5.171 PipelineRunner. `vv_work:ReadCtfDataFilter/results/compare_ab_2026-07-24.txt` records four cases: `toy_ff`, `toy_tt`, `cugrid_tt`, and `toy3d_ff`. Four of four cases match outside D1 and D2; none matches in every array because each contains unindexed points. The Cugrid comparison has 116,050 phase differences and 116,050 Euler-component differences, with the other 543,950 Euler components equal. The 3D case has two phase differences and 24 equal Euler components. + +The same ReadMe records two stock crashes for missing BS and empty phase definitions, and acceptance of phase 7 with only two phases. The 2026-09-17 local proof version adds three guards, rejects those malformed inputs, and preserves 60 datasets across four well-formed cases. Evidence: `vv_work:ReadCtfDataFilter/LegacyPatch.md`, `vv_work:ReadCtfDataFilter/scripts/run_legacy_proof_guard.py`, and `vv_work:ReadCtfDataFilter/scripts/run_wellformed_proof.py`. `vv_work:ReadCtfDataFilter/results/release_matrix.md` separately records single-slice output in DREAM3D-NX v7.4.1 and full-volume output in v7.4.2. + +The algorithm changed after the stock comparison. The relevant history is `4ec21e93f` (2026-09-20, bulk storage access), `a33c370ca` (2026-09-25, messaging API), and `84c66066a` (2026-10-15, 6/m alignment). The same-day V&V change is `1d02339d5` (2026-07-24). The July comparison does not cover the current algorithm. Pipelines and comparison code remain at `vv_work:ReadCtfDataFilter/pipelines/` and `vv_work:ReadCtfDataFilter/scripts/compare_ab.py`. -Skipped — documented reason (same rationale as `read_ang_test.md`): the filter's value-add is deterministic data plumbing (grid math from header values, verbatim column copies, a documented HKL Laue-group enum mapping, and two elementwise angle transforms whose expected values are mechanically derivable IEEE-754 roundings). There is no design freedom for the author-bias failure mode the review guards against; the fixture and every derivation are visible in the test source for any reviewer to audit at sign-off. +Release exposure was checked in both `Filters/ReadCtfDataFilter.cpp` and `Filters/Algorithms/ReadCtfData.cpp` under `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/` on all ten `upstream/release/v` sources from v7.0.0 through v7.4.2. -## Circular-oracle note +| Defects | Affected DREAM3D-NX releases | Source evidence | +|---|---|---| +| D3 | v7.0.0 through v7.4.1 | Missing buffer and phase checks; v7.4.2 contains `-19600` through `-19605`. | +| D4 | v7.0.0 through v7.4.1 | Z extent fixed to 1; v7.4.2 reads ZCells and ZStep. | +| D5 | v7.0.0 through v7.4.2 | Only Hexagonal_High receives the shift; `84c66066a` adds Hexagonal_Low. | +| D6 | v7.0.0 through v7.4.1 | Float32 radian constant; v7.4.2 uses float64 intermediates. | +| D7 | v7.0.0 through v7.4.1 | Error-code member used without fallback; v7.4.2 retains a negative return code. | + +The DREAM.3D `v6.5.171` source at `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/ReadCtfData.cpp` contains D3 and D5. It reads the full Z extent, uses double angle constants, and propagates the reader return code, so D4, D6, and D7 do not affect it. `Source/SIMPLib/Math/SIMPLibMath.h` declares the radians constant as double at SIMPL snapshot `0afaa6854ac0ef27df17278e11be711f74f77b90`, selected as the last revision before the DREAM.3D tag date, 2026-05-27. + +## Oracle provenance (Classes 2, 3, 5 only) + +Not applicable. The correctness oracles are Classes 1 and 4 in the test source. EbsdLib parses the input; its own output is not the expected-value oracle. + +## Second-engineer oracle review -This retirement **removes** a legacy-generated oracle. The replacement was derived by hand (and independently with NumPy for the two angle transforms) from the fixture text before any DREAM3D run — verified order: oracle encoded and reconciled first (2026-07-24, zero discrepancies), then the 6.5.171 comparison (2026-07-24; bit-identical numerics except the documented unindexed-point deviations D1/D2). +- **Reviewer:** Jared Duffey reviewed the V&V work in PR #1692. +- **Date:** 2026-07-28. +- **Skip reason:** Separate oracle re-derivation was skipped because the expected values follow direct copies, header metadata, and elementwise angle formulas. -## Archive disposition +## Regenerated to fix a circular-oracle situation? -The tar.gz remains in the GitHub [Data_Archive release](https://github.com/BlueQuartzSoftware/simplnx/releases/tag/Data_Archive) for historical checkouts of older commits; it must not be referenced by new code. +The archive was retired rather than regenerated during V&V. Inline inputs and analytical expectations replaced the stored-output comparison. The Cugrid input remains a migration-comparison fixture in `vv_work:ReadCtfDataFilter/ReadMe.md`. diff --git a/src/Plugins/OrientationAnalysis/vv/provenance/ReadH5OinaDataFilter.md b/src/Plugins/OrientationAnalysis/vv/provenance/ReadH5OinaDataFilter.md index fabbb108d3..5de4776aa4 100644 --- a/src/Plugins/OrientationAnalysis/vv/provenance/ReadH5OinaDataFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/provenance/ReadH5OinaDataFilter.md @@ -1,15 +1,6 @@ -# Exemplar Archive Provenance: ReadH5OinaDataFilter +# Exemplar Archive Provenance: H5Oina_Test_Data.tar.gz -This sidecar documents the test data behind `ReadH5OinaDataFilter` and the disposition -of each part of it. - -**Working folder.** Every file named in this document under "Recorded output", "Supporting -evidence" or "Suite logs" lives in the V&V working folder `ww_work/ReadH5OinaData/`, which -is **not committed to this repository**; it is archived to the V&V working-folder remote. -The committed deliverables are this sidecar, `vv/ReadH5OinaDataFilter.md` and -`vv/deviations/ReadH5OinaDataFilter.md`. - ---- +This sidecar records the vendor input, independent oracle, and source evidence for `ReadH5OinaDataFilter`. ## Archive identity @@ -17,202 +8,134 @@ The committed deliverables are this sidecar, `vv/ReadH5OinaDataFilter.md` and |---|---| | **Archive** | `H5Oina_Test_Data.tar.gz` | | **SHA512** | `346573ac6b96983680078e8b0a401aa25bd9302dff382ca86ae4e503ded6db3947c4c5611ee603db519d8a8dc6ed35b044a7bfea9880fade5ab54479d140ea03` | -| **`download_test_data()` entry** | `src/Plugins/OrientationAnalysis/test/CMakeLists.txt:146` | -| **Contents** | `H5Oina_Test_Data.h5oina`, `H5Oina_Test_Data.dream3d`, `H5Oina_Test_Data.xdmf`, `Acknowledgements.md` | -| **Used by** | `test/ReadH5OinaDataTest.cpp::"Real AZtec File Readback"` (the `.h5oina` only) | -| **Generated by** | **Not generated for this V&V.** The archive predates it and is unchanged, so there is no generating pipeline, script or commit to record on this side. The `.h5oina` is a vendor export, not a generated file; the `.dream3d` and `.xdmf` were produced by running this filter over it, which is why they are no longer consulted (below). | -| **Generated on / at commit** | Unknown — the archive was uploaded before the provenance convention existed. Its identity is pinned by the SHA512 above, which is byte-for-byte the value in `CMakeLists.txt`. | -| **Canonical oracle output** | **None from this archive.** The oracle for this filter is analytical: expected values are derived from the fixture specification in the test source, and the production file is compared against a readback of its own bytes. See "Replacement oracle" below. | -| **Changed?** | **No.** The archive is unchanged and was not re-uploaded. | +| **Used by tests** | `OrientationAnalysis::ReadH5OinaDataFilter: Real AZtec File Readback` | +| **Generated by** | Michael Jackson (inferred from PR #700) | +| **Generated on** | 2023-09-25 to 2024-03-26 (inferred from PR #700) | +| **Generated at commit** | `a51dd5f3d` (PR #700 merge, inferred) | + +`src/Plugins/OrientationAnalysis/test/CMakeLists.txt` registers this archive and SHA512. The first registration is `a51dd5f3d`, `FILTER: Add H5OINAReader (#700)`, authored by Michael Jackson. This identifies the archive registration; it does not date the vendor acquisition. -## The input file: retained +## How it was generated -`H5Oina_Test_Data.h5oina` is a genuine Oxford Instruments AZtec export and is retained as -an irreplaceable piece of production realism. Its properties, read directly with h5py: +The archive contains an Oxford Instruments AZtec vendor export, `H5Oina_Test_Data.h5oina`, and a `.dream3d` output generated by this filter. The test now reads only the vendor input and derives its expected values independently. `WriteH5OinaFixture` in `src/Plugins/OrientationAnalysis/test/ReadH5OinaDataTest.cpp` writes the small analytical inputs from explicit C++ scan specifications. The separate derivation script is `vv_work:ReadH5OinaDataFilter/h5oina_oracle.py`. -| Property | Value | +The vendor-file structure and values are recorded in `vv_work:ReadH5OinaDataFilter/probe_real_file.txt` and `vv_work:ReadH5OinaDataFilter/readback_real_file.txt`. + +| Vendor-file property | Recorded value | |---|---| -| `Format Version` | `5.0` | -| Scans | one, named `1` | -| Grid | `X Cells` 25, `Y Cells` 25 (625 points), `X Step` = `Y Step` = 4.0 | -| Phases | one group, named `1`: "Titanium cubic", Laue group 11 (Cubic-High), space group 229 | -| Lattice | dimensions 3.192, 3.192, 3.192; angles 1.5707964 rad each (90 degrees) | -| Phase column | values {0, 1} — the file contains both indexed and unindexed points | -| Euler | float32 (625, 3) in radians, observed range [0, 6.2773] | -| Extra columns | `Beam Position X`/`Y`, `Detector Distance`, `Pattern Center X`/`Y`, `Pattern Quality`, and a 625 × 512 × 622 `Processed Patterns` dataset — none of which this filter reads | - -Recorded probe output: `probe_real_file.txt`. - -Note that the file carries `Processed Patterns` and **no** `Unprocessed Patterns`, while the -filter only ever targeted the latter — one of the observations behind deviation D4. - -## The exemplar file: no longer consulted - -`H5Oina_Test_Data.dream3d` is **no longer used as a comparison target**. It remains inside -the archive because the archive is unchanged, but no test reads it. - -**Why.** The file was produced by running this filter on the sibling `.h5oina`, so it is a -self-oracle. Its `Euler` array is bit-identical to the raw file data — all 1,875 values, -compared as `uint32` bit patterns — which is what a verbatim copy of that column should -produce; the remaining eight cell arrays were not measured for bit-identity, because the -file's disposition does not turn on it. It is *not* a forbidden legacy oracle — no legacy -H5OINA importer has ever existed, so no version of DREAM3D could have generated it — but it -pins "the filter keeps doing what it did", not "the filter is right", which is the same -failure mode. - -It also had the pre-correction behavior baked in: its `LatticeConstants` reads -`[3.192, 3.192, 3.192, 1.5707964, 1.5707964, 1.5707964]`, the radian angles of deviation -D6. A test comparing against it did not merely fail to detect D6 — it actively enforced it. -Recorded measurement: `exemplar_selforacle_check.txt`. - -Its discriminating power against the thirteen deviations is limited: the file has -a single cubic phase, so the hexagonal alignment (D1, D3) never executes; it holds one scan, -so the slab offsets (D2), the stacking order (D9) and the multi-scan ensemble write (D8) are -never reached; pattern import was off, so D4 and its latent type mismatch were never -reached; and its angles are all equal, so the gamma slot (D5) is invisible. What it did pin -— single-scan copy plumbing, geometry wiring, the phase widening and the ensemble slot-0 -defaults against a real-world file — is preserved and strengthened by its replacement. - -## Replacement oracle - -**Toy fixtures, written by the test at run time.** `test/ReadH5OinaDataTest.cpp` declares a -fixture specification as C++ structs and writes `.h5oina` files with `H5Support::H5Lite` -into the binary test-output directory. The test cases create valid inputs and focused guard -inputs at run time. Nothing is committed as binary test data and no archive upload was -needed. The valid files carry the complete dataset set that `H5OINAReader` requires. The -guard files omit a required dataset or contain a value that the filter must reject. - -Every expected value is derived from the fixture specification. The hexagonal-alignment -expectations are the correctly-rounded float32 results of `double(φ2) + 30 × (π/180)`, -derived independently with IEEE-754 semantics in NumPy and embedded as literals with -derivation comments beside them. - -**Class 2 independent readback for the production file.** -`…::"Real AZtec File Readback"` compares all nine cell arrays element-wise against a readback -of the `.h5oina`'s own `Data` datasets performed with `H5Lite` — the file bytes, bypassing -`H5OINAReader` entirely — for 6,966 assertions. The geometry and the three ensemble arrays -are pinned as literals derived from the h5py readback. - -## Independent-derivation script +| Format Version | 5.0 | +| Scan | `1` | +| Grid | 25 by 25 points; X and Y step 4.0 | +| Phase group | `1`, Titanium cubic | +| Laue group / space group | 11 (Cubic-High) / 229 | +| Lattice dimensions | 3.192, 3.192, 3.192 | +| Lattice angles in the input | Three float32 values near pi/2 radians | +| Phase column | 587 indexed points with value 1; 38 unindexed points with value 0 | +| Euler dataset | 625 tuples, 3 float32 components, radians | +| Pattern dataset | Processed Patterns, shape 625 by 512 by 622; not imported by this filter | -| Field | Value | +`vv_work:ReadH5OinaDataFilter/error_column_check.txt` gives 587 Error values of 1 and 38 values of 2. The archive input therefore covers both indexed and unindexed points. Its one cubic phase cannot exercise hexagonal alignment or multi-scan behavior; the analytical fixtures provide those cases. + +## Canonical oracle output + +No generated `.dream3d` array in the archive is a canonical oracle. The test compares imported arrays with explicit fixture values or independent reads of the input HDF5 datasets. + +| DataPath | Source of expected values | |---|---| -| **Script** | `h5oina_oracle.py` | -| **Interpreter** | `/opt/local/anaconda3/envs/dream3d/bin/python`, h5py 3.16.0, NumPy 2.5.2 | -| **Author** | Michael Jackson | -| **Date** | 2026-08-24 | -| **`spec` mode** | Prints the fixture specification and every derived expectation as C++-ready float32 literals. Recorded output: `oracle_spec.txt`, reproduced bit-for-bit as `oracle_spec_rerun.txt`. This is the source of the pinned hexagonal-alignment constants. | -| **`readback` mode** | Re-reads an `.h5oina` with h5py and re-derives the expected NX arrays from the documented rules, without EbsdLib or simplnx. Recorded output for the production file: `readback_real_file.txt`. | - -The script encodes the derivation rules read out of the filter and algorithm source — geometry -from the header, slab placement, verbatim copies, the phase widening, the Laue-group mapping, -the ensemble slot-0 defaults, the radians-to-degrees lattice-angle conversion and the -hexagonal φ2 alignment — and applies them to the fixture specification or to a file's bytes. -It never runs the filter. - -## Oracle and dependency versions - -| Boundary | Version | +| `/ImageGeom/Cell Data/Band Contrast`, `Band Slope`, `Bands`, `Error` | Class 1 literals for synthetic scans; Class 2 H5Lite reads of `/1/EBSD/Data/*` for the vendor file | +| `/ImageGeom/Cell Data/Euler` | Class 1 exact float32 literals with pi/6 alignment for hexagonal points; raw HDF5 values for the cubic vendor input | +| `/ImageGeom/Cell Data/Mean Angular Deviation`, `X`, `Y` | Class 1 literals or independent HDF5 reads | +| `/ImageGeom/Cell Data/Phase` | Input bytes, retained as uint8 or widened to int32 | +| `/ImageGeom/Cell Ensemble Data/CrystalStructures` | Explicit Laue-group mapping and the unknown-structure sentinel at tuple 0 | +| `/ImageGeom/Cell Ensemble Data/MaterialName` | Explicit phase names and Invalid Phase at tuple 0 | +| `/ImageGeom/Cell Ensemble Data/LatticeConstants` | Input lengths and independently derived degree-valued angles; six zeros at tuple 0 | + +The paths above use the test's output geometry and attribute-matrix names. `UnitTest::CheckArraysInheritTupleDims` checks the tuple-shape invariant. The conversion-option test also verifies that alignment leaves cubic and unindexed Euler values unchanged. + +| Analytical fixture | Independent expectations | |---|---| -| **EbsdLib** | 3.1.2. EbsdLib is part of the system under test. Six of the thirteen deviations are corrections in `H5OINAReader`. | -| h5py / NumPy | 3.16.0 / 2.5.2, under `/opt/local/anaconda3/envs/dream3d/bin/python` | -| HDF5 | as vendored by the `NX-Com-Qt69-Vtk96-Rel-EbsdLib` preset's vcpkg manifest | +| A: one 3 by 2 scan | Unequal X/Y/Z dimensions, zero origin, distinct steps and Euler components; Hexagonal-High and cubic phases plus unindexed points | +| A option variant | Adds Hexagonal-Low; checks both alignment states and both Phase output types | +| B: two 2 by 2 cubic scans | Distinct cell values identify every scan slab; all 24 Euler values are checked | +| C: two 2 by 2 hexagonal scans | Distinct phi2 values check one alignment per point; current tests include both hexagonal classes | +| Boundary fixture | Two 65,537-point scans distinguish batch ends, partial tails, and scan transitions; this is a hidden test | + +The float32 inputs 0.1, 0.34, 0.09, 0.22, and 0.35 distinguish double-intermediate alignment from float-intermediate alignment. Values 0.25, 0.5, and 0.75 are control values. `vv_work:ReadH5OinaDataFilter/pi_over_6_discriminator.txt` records the discriminator search and confirms that `30 * pi/180` equals the nearest double to pi/6. `vv_work:ReadH5OinaDataFilter/oracle_spec.txt` supplies the explicit expected values; `vv_work:ReadH5OinaDataFilter/oracle_spec_rerun.txt` repeats them. + +## Oracle provenance (Classes 2, 3, 5 only) -## Supporting evidence +The Class 2 test reads the input datasets with H5Support::H5Lite and bypasses EbsdLib H5OINAReader. The separate Python script uses h5py for file reads and NumPy float32/float64 arithmetic for expected values. It does not execute the filter. The recorded vendor readback is independent of the generated `.dream3d` exemplar. -| File | What it records | +- **Library:** H5Support::H5Lite for the encoded readback; h5py and NumPy for the independent Python derivation. +- **Runtime:** C++ unit test and Python derivation script. +- **Random seed:** Not applicable; the fixtures use explicit values. +- **Script:** `vv_work:ReadH5OinaDataFilter/h5oina_oracle.py` (`spec` and `readback` modes). +- **Recorded output:** `vv_work:ReadH5OinaDataFilter/oracle_spec.txt` and `vv_work:ReadH5OinaDataFilter/readback_real_file.txt`. + +The script's hexagonal condition covers Hexagonal-High. The current C++ option and multi-scan tests extend the same independent pi/6 expectations to Hexagonal-Low. EbsdLib is part of the system under test, not the reference implementation. + +All pass in CI at `419f9db35`. The supplied CI evidence covers the 20 ordinary test cases on Linux, macOS, and Windows with in-core builds. The test file contains 21 `TEST_CASE` declarations, including one hidden boundary case. + +Release exposure comes from the filter and algorithm source at each release, plus the dependency source selected by the release manifest and registry. No release date or `REL:` commit is used to decide whether a defect is present. + +| DREAM3D-NX source | EbsdLib selected by manifest and registry | +|---|---| +| `upstream/release/v7.0.0` | 1.0.35 | +| `upstream/release/v7.0.1` | 1.0.36 | +| `upstream/release/v7.0.2` | 1.0.36 | +| `upstream/release/v7.0.3` | 1.0.36 | +| `upstream/release/v7.2.0` | 1.0.37 | +| `upstream/release/v7.2.1` | 1.0.38 | +| `upstream/release/v7.2.2` | 1.0.39 | +| `upstream/release/v7.4.0` | 1.0.40 | +| `upstream/release/v7.4.1` | 2.1.0 | +| `upstream/release/v7.4.2` | 3.1.2 | + +The v7.4.2 registry baseline lists EbsdLib 3.1.1, but `vcpkg.json` requires at least 3.1.2. Its registry includes the 3.1.2 port, tree `3fd1d7d7a1ac2c605f82bc88c7f7adcc5de7da9c`, which fetches tag `v3.1.2`. EbsdLib tags v1.0.35 through v1.0.40 and v2.1.0 retain the six documented reader defects; tag v3.1.2 contains their corrections in `Source/EbsdLib/IO/HKL/H5OINAReader.cpp`. + +| IDs | Source evidence | |---|---| -| `pi_over_6_discriminator.txt` | The exhaustive sweep of float32 values in `[0.25, 6.5)` that identified which φ2 values separate a double-precision intermediate from a float32 one (8,289,627 of 38,797,312 do), and confirmation that `30 × k_PiOver180D` is bit-identical to the nearest double to π/6 | -| `probe_real_file.txt` | The h5py structure dump of the production `.h5oina` | -| `error_column_check.txt` | The `Error` column census of the production file — 587 points at 1, 38 at 2, none at 0 — behind the documentation's masking correction | -| `exemplar_selforacle_check.txt` | The measurement behind the exemplar's retirement: `Euler` bit-identity and the radian `LatticeConstants` | -| `mutation_table.md` | Test-sensitivity evidence for the original seven temporary defects. Internal review added four detected defects for phase-group parsing, phase-definition validation, spacing validation, and error context. | +| D1, D2, D3 | `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ReadH5OinaData.cpp`: releases v7.0.0 through v7.4.1 add 30.0, copy Euler with a tuple offset, and align from tuple zero; v7.4.2 corrects all three | +| D4 | `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ReadH5OinaDataFilter.cpp`: earlier releases diagnose absent patterns; v7.4.2 explicitly diagnoses unsupported import | +| D5, D6 | EbsdLib `H5OINAReader.cpp`: earlier dependencies copy angles 0, 1, 1 without conversion; v3.1.2 copies 0, 1, 2 and converts radians to degrees | +| D7, D12 | EbsdLib `H5OINAReader.cpp`: v3.1.2 checks phase reads and validates canonical group names before opening groups | +| D8 | Filter preflight in v7.4.2 checks every phase index, count, and definition before shared ensemble writes | +| D9 | The v7.4.2 algorithm reverses selected scans for High To Low; earlier release algorithms call the shared list-order execution | +| D10 | EbsdLib v3.1.2 stores stream contents and includes nested header/data errors | +| D11 | Earlier EbsdLib converts signed counts to size_t and returns `-301` after storing `-90301` when the count product is zero; v3.1.2 validates signed counts and returns the stored code. Filter preflight adds `-9584` in v7.4.2 | +| D13 | The v7.4.2 filter requires finite, positive spacing on all axes | +| D14 | All releases through v7.4.2 test only Hexagonal-High; `84c66066a` (PR #1781) uses the shared predicate for both hexagonal classes | +| D15, D16 | The v7.4.2 filter validates later scan grids and every selected scan name; the earlier releases only read the first header in preflight | +| D17, D18 | The v7.4.2 algorithm adds extent and cell-Phase range checks; earlier releases copy and index without these checks | + +The primary filter V&V correction is `0f5f08e26` (PR #1728). D15 through D18 assign stable IDs to defects previously described in the sidecar's malformed-input table. They do not represent additional implementation changes. -## Suite logs +DREAM.3D tag `v6.5.171` contains no H5OINA importer. The same search finds no H5OINA filter in SIMPL `0afaa6854` or the plugin snapshots selected on or before the tag date, 2026-05-27. Those plugin snapshots include DREAM3DReview `8bef829b6`, ECAccess `4d6e07ba6`, ITKImageProcessing `7fe4ef67c`, NETLIntegration `4f60057bd`, SimulationIO `6df7a7abb`, UCSBUtilities `d7d57836a`, and VolumeMeshing `5b36e86b0`. DREAM.3D v6.5.171 was not affected by any listed defect because it has no corresponding code path. -| File | What it records | +There is no legacy comparison run or run date. The artifacts are independent-oracle evidence, not a DREAM.3D comparison. + +The saved mutation evidence is `vv_work:ReadH5OinaDataFilter/mutation_table.md`. It records seven isolated mutations, each detected by the named test. The seventh used the filter extent guard before the current EbsdLib rejection moved the short-data failure earlier. + +| Temporary defect | Recorded detector | |---|---| -| `red_baseline.log` | The RED baseline: the full suite run against the batch-base sources with the oracle suite in place — 14 of 16 failing, including the SEGFAULT that is deviation D7 | -| `fixround1_red_baseline.log` | The RED baseline for the multi-scan phase-group guards and the stacking-order implementation: three of 17 failing before those changes | -| `fixround1_green_readh5oina.log` | The green `ctest -V` run of the filter's suite, 17 of 17 | -| `fixround1_oa_suite.log` | The full `OrientationAnalysis::` suite at the verified tree state, after a full build of the preset directory: 308 tests, one failure | -| `fixround1_simplnxcore_suite.log` | The full `SimplnxCore::` suite at the same state | -| `gate_oa_head.log`, `gate_oa_ebsdlib539.log`, `gate_head_fail_names.txt`, `gate_539_fail_names.txt` | The controlled EbsdLib rollback described below | -| `ebsdlib_suite.log`, `simplnxcore_suite.log` | Earlier EbsdLib and SimplnxCore suite runs | - -### The pre-existing OrientationAnalysis failure - -**After a full build of the preset directory (`cmake --build . --target all`), the -`OrientationAnalysis::` suite is 308 tests with exactly one failure: -`OrientationAnalysis::ComputeSchmidsFilter`, on `Schmid_Lambdas`.** That is the known -EbsdLib-staging numerical drift from the `CubicOps` precision correction; it reads no -`.h5oina` file and shares no code with this filter. Log: `fixround1_oa_suite.log`. - -Two earlier runs of the same suite in the same directory recorded **twenty-nine** failures: -`ComputeSchmidsFilter` plus twenty `PIPELINE::OrientationAnalysis::` entries and eight -`PY::OrientationAnalysis::` entries. Those twenty-eight are **artifacts of a partially built -directory, not defects.** `PIPELINE::` tests invoke `Bin/nxrunner` and `PY::` tests invoke -the Python bindings, and neither target is built by -`cmake --build . --target OrientationAnalysisUnitTest`, which is what those runs had built. -Once `--target all` rebuilt `nxrunner` against the current libraries, all twenty-eight -passed with no source change. Any claim about this build directory's baseline has to be -measured after a full build. - -The one genuine failure is shown to be independent of the H5OINA corrections by a -**controlled rollback of the EbsdLib side alone**, which is the only variable in them that -could reach an unrelated test: - -| Configuration | Result | Non-H5OINA failure set | -|---|---|---| -| simplnx and EbsdLib both at the batch head | 307 tests, 29 fail | 29 names -- `gate_head_fail_names.txt` | -| simplnx at the batch head, EbsdLib rolled back to `539ddfc` (the batch's EbsdLib base) | 307 tests, 32 fail | **the same 29 names -- `diff` of the two sorted lists is empty** | - -Logs: `gate_oa_head.log` and `gate_oa_ebsdlib539.log`, with the sorted name lists in -`gate_head_fail_names.txt` and `gate_539_fail_names.txt`. Both runs were made in the -partially built state described above, so their absolute counts are inflated; what the -comparison establishes is that rolling EbsdLib back changes the non-H5OINA failure set not -at all. The three extra failures at `539ddfc` are the three ReadH5Oina tests that the -EbsdLib corrections are required for -- `Class 1 Analytical Oracle`, `Real AZtec File -Readback` and `EbsdLib Error Passthrough - Missing Lattice Angles (-9582)`, the last as a -SEGFAULT -- which is evidence for the EbsdLib dependency. The -structural argument agrees: `git diff --stat 539ddfc..HEAD` on EbsdLib is one file, -`Source/EbsdLib/IO/HKL/H5OINAReader.cpp`, with no header and no API change, so nothing -outside the H5OINA read path can be reached. - -The same applies to `SimplnxCore::`, which is **985 of 985 passing** after the full build -(`fixround1_simplnxcore_suite.log`). An earlier partially built run of it reported -`PIPELINE::SimplnxCore::002_ApplyTransformation_Image` failing — another `nxrunner` -pipeline test of exactly the shape described above. - -`Real AZtec File Readback` is intermittently flaky in this build dir on the test-data -sentinel extraction, not on any assertion; a failure of that shape clears on an immediate -re-run with no source change. - -The final internal-review run uses EbsdLib 3.1.2. The results are 20 of 20 H5OINA tests, -408 of 408 EbsdLib tests, and 996 of 996 SimplnxCore tests. The OrientationAnalysis suite -passes 281 of 282 tests. The only failure is the unrelated ComputeSchmids archive -comparison against values from before its precision correction. - -## Oracle-before-comparison ordering - -There was no comparison to order against: DREAM3D 6.5.171 has no H5OINA importer. The oracle -was nonetheless established before any filter output was observed — the fixture -specification, the derivation script and every pinned literal were written and recorded -(`oracle_spec.txt`, timestamped before the first build) ahead of the first test run, and the -first run of the suite against the batch-base code is the RED baseline in `red_baseline.log`. -No expected value in the test file was taken from observed output. +| Restore the 30-radian addend | Analytical oracle, conversion options, multi-scan alignment, and format variants | +| Use a float32 alignment intermediate | The same four test cases | +| Use the tuple offset as the Euler element offset | Multi-scan placement and alignment | +| Align the first slab for every scan | Multi-scan alignment | +| Copy beta into the gamma slot | Analytical oracle | +| Keep lattice angles in radians | Analytical oracle and vendor readback | +| Remove the filter extent guard | Extent-mismatch case before EbsdLib added its earlier rejection | + +`vv_work:ReadH5OinaDataFilter/red_baseline.log` records the missing-lattice-angle crash and the failed guard cases before the fixes. `vv_work:ReadH5OinaDataFilter/fixround1_red_baseline.log` records the multi-scan phase and stacking failures. `vv_work:ReadH5OinaDataFilter/fixround1_green_readh5oina.log` records their corrected behavior. These artifacts support the defect mechanisms; the current test-pass statement uses the supplied CI evidence. ## Second-engineer oracle review -Outstanding — to be recorded at PR review. The oracle design is fully auditable from the -committed test source: the fixture specification, the derivation of every pinned literal and -the reason each discriminating φ2 value was chosen are all stated in comments beside the -values. +- **Reviewer:** Pending second-engineer review. + +## Regenerated to fix a circular-oracle situation? + +No. The archive still supplies the vendor input, but its generated `H5Oina_Test_Data.dream3d` output is no longer used as an oracle. The unit test now uses analytical values and independent reads of the input file. -## Archive disposition +`vv_work:ReadH5OinaDataFilter/exemplar_selforacle_check.txt` records 1,875 Euler elements that are bit-identical to the raw input. It also records the old `LatticeConstants` angle components in radians. Comparing with that output enforced D6 instead of detecting it. The vendor input has equal lattice angles, one cubic phase, and one scan, so it cannot expose the gamma-slot error, hexagonal-alignment errors, or scan-placement errors. -`H5Oina_Test_Data.tar.gz` stays in the GitHub -[Data_Archive release](https://github.com/BlueQuartzSoftware/simplnx/releases/tag/Data_Archive) -and on the www.dream3d.io mirror unchanged, and its `download_test_data()` entry stays in -`test/CMakeLists.txt` because the `.h5oina` input is still needed. Should the archive ever be -regenerated, the `.dream3d` and `.xdmf` members can be dropped: nothing reads them. +The replacement retains the real-file geometry, array-copy, Phase-widening, and ensemble checks. The analytical fixtures supply distinct angles, multiple phases, and multiple scans to detect the defects the vendor input cannot expose. diff --git a/src/Plugins/OrientationAnalysis/vv/provenance/RotateEulerRefFrameFilter.md b/src/Plugins/OrientationAnalysis/vv/provenance/RotateEulerRefFrameFilter.md index 8cd4f40cff..74f4bba282 100644 --- a/src/Plugins/OrientationAnalysis/vv/provenance/RotateEulerRefFrameFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/provenance/RotateEulerRefFrameFilter.md @@ -1,74 +1,88 @@ -# Exemplar Provenance: RotateEulerRefFrameFilter +# Exemplar Archive Provenance: ASCIIData.tar.gz -This sidecar documents the oracle sources and archive provenance for the `RotateEulerRefFrameFilter` V&V cycle (2026-07-03). **No new exemplar archive was created** — the Class 1 oracle is encoded entirely as inline expected values in the test source, and the Class 4 invariants require no expected output. +This sidecar records the shared regression archive, independent oracle and release comparison evidence for `RotateEulerRefFrameFilter`. ## Archive identity -| | | +| Field | Value | |---|---| -| Archive | `ASCIIData.tar.gz` (pre-existing, shared across plugins) | -| Declared in | `src/Plugins/SimplnxCore/CMakeLists.txt` (`download_test_data` entry ~line 523) | -| Generated by | Legacy DREAM3D era (pre-NX); exact engineer/date unrecorded | -| Consumed by (this filter) | `OrientationAnalysis::RotateEulerRefFrame` (legacy-parity regression pin) | -| Files used | `ASCIIData/EulerAngles.csv` (480k-tuple input), `ASCIIData/EulersRotated.csv` (comparison values) | +| **Archive** | `ASCIIData.tar.gz` | +| **SHA512** | `70388864301ca1ea7fce7b1666d3abf682eee68c7d8b7a9bf532df7aff11e7ea9de7dc2dc80e33f0e363cbad023b663bff97df4be362f0312d311e9d5bedf370` | +| **Used by tests** | `OrientationAnalysis::RotateEulerRefFrame` | +| **Generated by** | Jessica Marquis (inferred from PR #572) | +| **Generated on** | 2023-04-20 to 2023-04-27 (inferred from PR #572) | +| **Generated at commit** | `e3f4f593c` (PR #572 merge, inferred) | -### Circular-oracle note +The archive and hash are registered in `src/Plugins/SimplnxCore/CMakeLists.txt:554-555`, not a plugin `test/CMakeLists.txt`. PR #572 first registers this archive name and hash in the former `src/Plugins/ComplexCore/CMakeLists.txt`. It replaces `ASCII_Data.tar.gz` and adds a rectilinear-grid CSV. These inferred identity fields describe the shared package registration. -`EulersRotated.csv` was generated by running the legacy DREAM3D implementation (axis (1,1,1), 30°) on `EulerAngles.csv`. Under the v2 policy this is **not a valid correctness oracle** ("legacy produced this output"). This V&V cycle did **not** regenerate the archive; instead the test consuming it was reclassified in the V&V report as a *legacy-parity regression pin* (large-array + parallel-path coverage), and correctness was established independently by the Class 1 oracle below. The comparison remains valuable precisely because it pins parity with legacy behavior at 480k-tuple scale. +## How it was generated -## Canonical oracle output (Class 1 — Analytical) +`ASCIIData/ReadMe.md` identifies Small IN100 as the source dataset. DREAM.3D cropped X indices 41–100, Y indices 41–120 and Z indices 0–99, giving 60×80×100 cells. It generated the rotated Euler values with a 30° rotation about `(1,1,1)`. The test imports `ASCIIData/EulerAngles.csv` and `ASCIIData/EulersRotated.csv`; it uses the latter for regression comparisons. -The oracle DataPaths are the 8 fixture expected values in -`test/RotateEulerRefFrameTest.cpp::AnalyticalFixtures::k_Fixtures` (F1–F8), each carrying its -derivation as a source comment. +The independent fixtures are separate from the archive. `AnalyticalFixtures::CreateEulerArray()` constructs the input arrays, and `AnalyticalFixtures::k_Fixtures` contains eight fixed expected triples. `vv_work:RotateEulerRefFrameFilter/rotate_euler_ref_frame_oracle.py` implements Rodrigues rotation and Euler conversion equations without importing EbsdLib or SIMPLNX. -**Derivation (first principles, independent of SIMPLNX / EbsdLib / legacy):** +## Canonical oracle output -1. Bunge passive convention: `v_crystal = g · v_sample`. -2. Rotating the sample reference frame by +w (right-hand rule) about unit axis n gives - new-frame coordinates `u_new = P(w) · u_old`, with `P(w)` the passive rotation matrix. -3. Therefore `g' = g · P(w)ᵀ = g · R_active(n, w)`, with `R_active` the standard Rodrigues - rotation matrix `R = cos(w)·I + sin(w)·[n]ₓ + (1−cos(w))·nnᵀ`. -4. Euler ↔ matrix conversions per Rowenhorst et al 2015, *Modelling Simul. Mater. Sci. Eng.* 23 - 083501 (doi:10.1088/0965-0393/23/8/083501), Eq. A.5 (eu2om) and Eq. A.9 (om2eu), outputs - canonicalized to `phi1, phi2 ∈ [0, 2π)`, `Φ ∈ [0, π]`. +| DataPath | Source of expected values | +|---|---| +| `CellData/EulerAngles` | Class 1: F1–F7 use closed-form derivations; F8 evaluates the same independent matrix equations in the NumPy script. The eight triples are stored in `src/Plugins/OrientationAnalysis/test/RotateEulerRefFrameTest.cpp`. | +| `CellData/EulerAngles` | Class 4: inclusive float32 range bounds, inverse-rotation recovery and 45°+45° equivalence to 90°. | +| `EulersRotated` | Values imported from `ASCIIData/EulersRotated.csv`. This is a DREAM.3D regression reference, separate from the independent oracle. | -Closed forms verified by hand: Z-axis rotation ⇒ `phi1' = phi1 − w (mod 2π)` with Φ/phi2 unchanged -(F1–F3, F6); identity orientation + axis-angle ⇒ `g' = R_active(n, w)` with om2eu evaluated by hand -(F4, F5, F7). F8 (general orientation, Y-axis 45°) has no closed form and is script-derived. +For Bunge passive orientations, `v_crystal = g · v_sample`. A reference-frame rotation gives `g′ = g · R_active(n,w)`, where Rodrigues' formula defines `R_active`. A Z-axis rotation therefore subtracts `w` from `phi1` modulo `2π`. F4 and F5 check the same X rotation with unit and non-unit axes; F7 checks the `(1,1,1)` axis. F8 uses unequal Euler components `(0.8,1.1,0.4)` and a 45° Y rotation. -## Oracle provenance block (Class 2-style cross-check script) +F1 is the zero-Phi singular case. F6 is a zero-angle rotation with input Phi equal to 1 radian, so it does not exercise that singularity. The analytical test uses `1e-5` rad tolerance. The round-trip and composition checks use `1e-4` for two float32 store/load passes. Float32 rounding makes the range limits inclusive. -| | | -|---|---| -| Script | `rotate_euler_ref_frame_oracle.py` — in `Code_Review/RotateEulerRefFrame/`, uploaded to the OneDrive verification archive (2026-07-08) | -| Library + version | numpy 2.0.2 (pure-numpy; equations typed from Rowenhorst 2015, not imported from any orientation library) | -| Random seed | none — all fixtures deterministic | -| What it does | Implements the first-principles derivation above; asserts all seven closed-form claims; asserts the Class 4 invariants (round-trip, composability); emits the F1–F8 expected-value table pasted into the C++ test source | +The five ordinary test cases pass on Linux, macOS and Windows in the supplied CI evidence. All pass in CI at `419f9db35`. + +**Legacy comparison evidence** + +`vv_work:RotateEulerRefFrameFilter/legacy_comparison_summary.md` records the initial run on 2026-07-03 and the release comparison on 2026-09-17. It identifies the stock DREAM.3D v6.5.171 runner; `vv_work:RotateEulerRefFrameFilter/results/release_matrix/logs/legacy_6_5_171.stdout.txt` confirms `SIMPLib Version 1.2.828.f45085c83`. `vv_work:RotateEulerRefFrameFilter/run_release_matrix.py` identifies the two NX runners and the pipelines. + +Run against DREAM.3D v6.5.171 on 2026-09-17: 6 of 6 cases match for each of DREAM3D-NX v7.4.1 and v7.4.2 at the declared `1e-5` rad tolerance. Each case uses 12 orientations from `vv_work:RotateEulerRefFrameFilter/euler_input.csv`. + +| Cases | Axis | Angle | +|---|---|---| +| C1, C2, C6 | `(0,0,1)` | 90°, 30°, 0° | +| C3 | `(2,0,0)` | 90° | +| C4 | `(0,1,0)` | 45° | +| C5 | `(1,1,1)` | 120° | + +`vv_work:RotateEulerRefFrameFilter/results/release_matrix/comparison.json` and `vv_work:RotateEulerRefFrameFilter/results/release_matrix/comparison.md` record maximum wrap-aware differences of `7.15255737e-7` rad between each NX release and DREAM.3D, `2.27283216e-7` rad between each NX release and the independent oracle, and `8.1150305e-7` rad between DREAM.3D and the oracle. + +The current algorithm changed after that run: `4ec21e93f` (2026-09-20, PR #1568) replaced the parallel kernel with serial buffers; `a33c370ca` (2026-09-25, PR #1697) changed progress messaging. The release comparison establishes results for the named releases only. -Drift-risk note: the script is a *cross-check* of hand derivations, not the primary oracle — the -primary oracle is the derivation itself, embedded as comments beside each fixture. numpy version -drift therefore cannot silently change the encoded expected values. +**Precision and representation notes** -## Legacy comparison artifacts +- `RotateEulerRefFrameFilter-N1`: the tagged DREAM.3D v6.5.171 source uses float angle conversion, `FOrientTransformsType` and float matrix operations. Current SIMPLNX uses double angle conversion and double orientation matrices. The measured differences above are within the test tolerance; the source does not support attributing them solely to angle conversion. +- `RotateEulerRefFrameFilter-N2`: the 2026-07-03 summary records `phi1` near 0 in SIMPLNX and near `2π` in DREAM.3D at an exact wrap boundary. Compare such angles modulo `2π`. +- `RotateEulerRefFrameFilter-N3`: the zero-axis validation difference is now recorded as the confirmed defect `RotateEulerRefFrameFilter-D1`. The original claim that DREAM.3D always divides a zero axis by zero is contradicted by its float normalization helper. -`Code_Review/RotateEulerRefFrame/` additionally holds: `euler_input.csv` (12-orientation shared -input), `generate_pipelines.py`, the generated SIMPL (`legacy_RotateEulerRefFrame_6cases.json`) and -NX (`nx_RotateEulerRefFrame_6cases.d3dpipeline`) pipelines, both output `.dream3d` files, -`compare_outputs.py`, and `legacy_comparison_summary.md` (results: no deviations; max wrap-aware -diff 7.2e-7 rad). +**Release-source evidence** -> **Evidence archival (done):** `Code_Review/RotateEulerRefFrame/` (input CSV, generator, both -> pipelines, both output `.dream3d` files, comparison script, and `legacy_comparison_summary.md`) was -> **uploaded to the OneDrive verification archive** on 2026-07-08, so the legacy A/B is reproducible -> and reviewable from the archive. The F8 oracle value and the 6-case comparison were additionally -> reproduced via scipy during review. +Every listed release was inspected at `upstream/release/v` using `RotateEulerRefFrameFilter.cpp` and `Algorithms/RotateEulerRefFrame.cpp` under `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/`. + +| Releases | Axis validation | Store scheduling | +|---|---|---| +| v7.0.0, v7.0.1, v7.0.2, v7.0.3, v7.2.0, v7.2.1, v7.2.2, v7.4.0 | No preflight axis check; `MatrixMath::Normalize3x1()` normalizes the axis. | Parallel dispatch without `requireArraysInMemory()`. | +| v7.4.1 | No preflight axis check; `FloatVec3::normalize()` divides by the magnitude. | Parallel dispatch without `requireArraysInMemory()`. | +| v7.4.2 | Preflight `-96200` and algorithm `-67050` guards are present. | `requireArraysInMemory()` precedes parallel dispatch. | + +PR #1654 (`11a7acdae`, 2026-07-13) adds the two axis guards and the residency safeguard. The current implementation retains the axis guards and uses serial buffered traversal. The zero-axis test requires failed preflight but does not assert an error code. There is no direct fixture for `-67050` or the former concurrent-access failure. + +DREAM.3D tag `v6.5.171` uses a resident raw float array in `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/RotateEulerRefFrame.cpp`. Its `dataCheck()` does not reject a zero rotation axis. SIMPL commit `0afaa6854`, the last commit on or before the tag date (2026-05-27), implements the float zero-denominator return in `Source/SIMPLib/Math/MatrixMath.cpp:357-365`. DREAM.3D v6.5.171 was affected by the missing input validation (D1), but not the nonresident-store access defect (D2). + +## Oracle provenance (Classes 2, 3, 5 only) + +The primary oracles are Classes 1 and 4. The independent NumPy script evaluates the matrix equations and cross-checks the analytical values. Its source cites Rowenhorst et al., “Consistent representations of and conversions between 3D rotations,” *Modelling and Simulation in Materials Science and Engineering* 23 (2015), 083501, equations A.5 and A.9, DOI `10.1088/0965-0393/23/8/083501`. All inputs are deterministic; no random seed is used. ## Second-engineer oracle review -- **Reviewer:** Michael Jackson (technical authority) -- **Date:** 2026-07-16 -- **Note:** Signed off. Review focus covered: (a) the F1–F7 hand derivations; (b) the - sign convention — a +w reference-frame rotation about Z *subtracts* w from phi1 - (`phi1' = phi1 − w`), which the first-principles derivation establishes and both implementations - exhibit; (c) the Class 4 invariant set is complete for this algorithm. +- **Reviewer:** Michael Jackson. +- **Date:** 2026-07-16. + +The existing oracle review records this name and date. The report sign-off names only Michael Jackson and does not identify a distinct second engineer. + +## Regenerated to fix a circular-oracle situation? + +No archive regeneration was part of this V&V. The archive remains a regression reference. Independent expected values and invariants are encoded in the test source instead of replacing archive values with SIMPLNX output. diff --git a/src/Plugins/OrientationAnalysis/vv/provenance/WritePoleFigureFilter.md b/src/Plugins/OrientationAnalysis/vv/provenance/WritePoleFigureFilter.md index 3793c70369..bafbaa7a8f 100644 --- a/src/Plugins/OrientationAnalysis/vv/provenance/WritePoleFigureFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/provenance/WritePoleFigureFilter.md @@ -1,8 +1,6 @@ # Exemplar Archive Provenance: Pole_Figure_Exemplars_v6.tar.gz -This sidecar records the exemplar archive and the Class 5 (expert-visual) oracle used to verify `WritePoleFigureFilter`. - ---- +This sidecar records the test inputs, oracle definitions and archived visual comparison for `WritePoleFigureFilter`. ## Archive identity @@ -10,42 +8,47 @@ This sidecar records the exemplar archive and the Class 5 (expert-visual) oracle |---|---| | **Archive** | `Pole_Figure_Exemplars_v6.tar.gz` | | **SHA512** | `23ad8853c38c66f498c7a9c43d6152419506b2bcb6c7e4854728dd3ad81efd309541981bf703ad9b0a1e9b506a0098da918599d5209cb06353281d9cecf39223` | -| **Used by tests** | `WritePoleFigureFilter: Mask filter changes the rendered pole figure`, `WritePoleFigureFilter: HexConvention choice reaches algorithm`, `WritePoleFigureFilter: Discrete mode and marker radius reach algorithm` | -| **Generated by** | BlueQuartz (existing exemplar; inputs only) | -| **Generated on** | pre-existing (consumed as-is for this V&V) | -| **Generated at commit** | n/a (inputs-only archive) | +| **Used by tests** | `OrientationAnalysis::WritePoleFigureFilter: Mask filter changes the rendered pole figure`, `OrientationAnalysis::WritePoleFigureFilter: HexConvention choice reaches algorithm`, `OrientationAnalysis::WritePoleFigureFilter: Discrete mode and marker radius reach algorithm` | +| **Generated by** | Michael Jackson (inferred from PR #1631) | +| **Generated on** | 2026-06-04 to 2026-06-11 (inferred from PR #1631) | +| **Generated at commit** | `ec22ac375` (PR #1631 merge, inferred) | ## How it was generated -The archive contains **inputs only**: a single-phase set of 502 hexagonal (Ti, Laue class 6/mmm) orientations under `Imported Data/Eulers` (radians), `Imported Data/Phases`, a 251-of-502 `Imported Data/Mask`, and `EnsembleAttributeMatrix/CrystalStructures` + `PhaseNames`. There is **no baked pole-figure image** used as an oracle — pixel-level reproduction is owned and byte-tested by EbsdLib upstream (`PoleFigureCompositorTest::All_Laue_Classes`), and baking an image here would couple simplnx CI to EbsdLib byte-identity (the prior v5 baseline-drift failure mode). +The archive supplies 502 hexagonal Ti orientations, phase IDs, a 251-of-502 mask, crystal structures and phase names. Tests load `Pole_Figure_Exemplars_v6/Pole_Figure_Exemplars_v6.dream3d` and use these arrays as inputs. Archive attribution comes from its first registration in `src/Plugins/OrientationAnalysis/test/CMakeLists.txt`; the archived comparison scripts are `vv_work:WritePoleFigureFilter/build_legacy_input.py`, `vv_work:WritePoleFigureFilter/gen_nx_pf.py` and `vv_work:WritePoleFigureFilter/gen_legacy_pf.py`. Those scripts reuse the orientation inputs and construct the legacy input container; they do not generate expected test outputs. ## Canonical oracle output -There is no gold-standard array in the archive. Correctness of the rendered pole figures is established by **Class 5 expert-visual** sign-off (below) plus **Class 4 invariants** encoded in the test source: +The tests do not read a stored image as the expected result. Three Class 4 fixtures compare outputs from paired parameter settings. The four default test cases, including SIMPL conversion, run in the supplied Linux, macOS and Windows in-core CI result. All pass in CI at `419f9db35`. -| Output | Source of expected values | +| DataPath | Source of expected values | |---|---| -| Composite RGB `Phase_N` image | Class 5 expert-visual sign-off vs legacy DREAM3D renders (hex + cubic); Class 2 pixel reproduction owned by EbsdLib `PoleFigureCompositorTest`. | -| Intensity arrays `Phase_N_` | Class 4 invariants (mask changes output; X‖a vs X‖a* rotates basal families 30°). | +| `//Cell Data/Phase_1` | Class 4: masking, basis convention, mode and marker radius must change more than 1% of RGB components. | +| `//Cell Data/Phase_1_<011>` | Class 4: the two hexagonal basis choices must change more than 1% of basal-intensity pixels. The test does not measure an exact rotation angle. | +| `/Counts/Cell Data/Phase_1_001`, `Phase_1_011`, `Phase_1_111` | Class 1 encoded in the hidden `real HDF5 65536-block discrete-count oracle`: exact sums [12, 24, 16] and a complete 32×32 <001> histogram. | + +The Class 1 fixture constructs its own inputs. Identity and a 45-degree Z rotation lie on opposite sides of the 65,536-tuple extraction boundary. A third orientation is masked out. The <001> image has four counts at (16,15), four cardinal equatorial counts and four diagonal counts after row reversal. This fixture is separate from the four default CI cases. + +## Oracle provenance (Classes 2, 3, 5 only) -## Oracle provenance — Class 5 (Expert-visual) +### Class 5 — Expert-visual -- **Approving expert:** Michael Jackson (OA domain expert) +- **Approving expert:** Michael Jackson - **Approval date:** 2026-07-01 -- **What was compared:** the same orientation input rendered through DREAM3D **6.5.171**, DREAM3D **6.5.172**, and **SIMPLNX (EbsdLib 3.1.0)**, in Color and Discrete modes, for **two crystal systems**: - - Hexagonal (the v6 archive's 502 Ti orientations) - - Cubic (the same 502 orientations relabeled `Cubic_High`, to exercise the `<001>/<011>/<111>` families and cubic Laue ops) -- **Signed-off outputs:** the full three-way set (6.5.171 / 6.5.172 / NX × color/discrete × hex/cubic) — hex: `out_legacy_6_5_171/{color,discrete}_Phase_1.pdf`, `out_legacy_6_5_172/…` (byte-identical to 6.5.171), `out_nx/nx_{color,discrete}_1.png`; cubic: `out_cubic_legacy_6_5_171/`, `out_cubic_legacy_6_5_172/` (color differs only in a trailing PDF metadata block; discrete byte-identical), `out_cubic_nx/`. -- **Evidence archival:** regenerated 2026-07-08. The **generator scripts, pipeline files, and a `README.md` are committed in-repo** under `Code_Review/vv/WritePoleFigure/`; the **binary evidence (input `.dream3d` files and the rendered PDFs/PNGs) is archived to OneDrive**, not committed (repo hygiene — see that folder's `.gitignore`). The comparison is fully reproducible from the committed scripts. The NX renders were produced by an `nxrunner` built from this branch (`topic/write_pole_figure_fixes`), so the discrete renders honor the `discrete_marker_radius` parameter. **Reconstruction note:** the legacy-format input (`pf_input_legacy.dream3d`) was re-minted by `build_legacy_input.py` (the original minting script was unrecoverable) from the NX inputs' 502 orientations, using the SIMPL v7.0 layout; both legacy engines read it and rendered valid pole figures, validating the reconstruction. -- **Acceptance criteria / finding:** the pole-figure **data** (pole positions, intensity distribution, color-intensity mapping) is visually identical across all three versions for both crystal systems. The only differences are the four documented, non-defect deviations (`WritePoleFigureFilter-D1..D4`: axis-label text, font, symmetry-equivalent family labels, and the intentional discrete vector-marker rendering). No correctness defect observed. -- **Class-5 justification:** legacy DREAM3D emits pole figures only as rasterized PDFs (no numeric ground truth), and per-Laue-class pixel rendering is owned/byte-tested by EbsdLib upstream, so no Class 1–3 oracle fully specifies the rendered image. The analytically-checkable wiring (mask, hex convention) is pinned by Class 4 invariants. +- **Signed-off outputs:** Color and Discrete figures for hexagonal and cubic data. The oracle review date below is 2026-07-16. The available render set was regenerated on 2026-07-08; see `vv_work:WritePoleFigureFilter/README.md`. +- **Class-5-only justification:** A pixel equality oracle cannot compare a PDF renderer with a different PNG renderer. The visual review evaluates pole positions, symmetry and color mapping; numerical and parameter-response checks are encoded separately. + +Run against DREAM.3D v6.5.171 on 2026-07-08: 4 of 4 hexagonal/cubic Color/Discrete cases match the expected pole-figure topology by visual review. The README identifies the stock application PipelineRunner. These are visual matches, not exact image matches; D1–D4 describe the rendering differences. The output files are `vv_work:WritePoleFigureFilter/out_legacy_6_5_171/color_Phase_1.pdf`, `vv_work:WritePoleFigureFilter/out_legacy_6_5_171/discrete_Phase_1.pdf`, and the corresponding files under `vv_work:WritePoleFigureFilter/out_cubic_legacy_6_5_171/`, `vv_work:WritePoleFigureFilter/out_nx/` and `vv_work:WritePoleFigureFilter/out_cubic_nx/`. + +DREAM.3D v6.5.171 and a local proof version were also compared after Poppler rasterization at 96 DPI. Three of four cases are pixel-identical. Cubic Color differs in 7 of 2,832,024 pixels within a 3×3 area; the maximum channel difference is 17. This is a pixel difference, not only PDF metadata. See `vv_work:WritePoleFigureFilter/results/legacy_render_comparison.md` and `vv_work:WritePoleFigureFilter/results/legacy_render_comparison.json`. + +Algorithm source changed after the run: `64f5f27cb` (2026-07-10, PR #1647, renderer configuration), `4ec21e93f` (2026-09-20, PR #1568, chunked input and output access), and `a33c370ca` (2026-09-25, PR #1697, messaging API). The 2026-07-08 comparison does not cover the current algorithm. Release-source inspection for D6–D9 uses all ten `upstream/release/v7.0.0` through `upstream/release/v7.4.2` snapshots; exact mechanisms are recorded in the deviation Evidence paragraphs. ## Second-engineer oracle review -- **Reviewer:** Michael Jackson (technical authority) +- **Reviewer:** Michael Jackson - **Date:** 2026-07-16 -- **Note:** Signed off — the four documented differences are confirmed the complete set and all non-defect. ## Regenerated to fix a circular-oracle situation? -No. The archive is inputs-only and pre-existing; no baked filter output is used as an oracle, so there is no circular-oracle situation to remediate. The legacy comparison input was authored independently — minted by `build_legacy_input.py` (image geometry + `MaterialName` StringDataArray, SIMPL v7.0 layout) from the same 502 orientations — not derived from any filter output. +No expected image baseline is used by the current tests. The Class 4 expectations are output-change invariants, and the hidden Class 1 expectations are constructed from cubic pole multiplicities. The comparison input uses the original Euler angles and does not derive its input from a filter output. diff --git a/src/Plugins/OrientationAnalysis/vv/provenance/neighbor_orientation_correlation_v2.md b/src/Plugins/OrientationAnalysis/vv/provenance/neighbor_orientation_correlation_v2.md index ea33a80072..c58bc92668 100644 --- a/src/Plugins/OrientationAnalysis/vv/provenance/neighbor_orientation_correlation_v2.md +++ b/src/Plugins/OrientationAnalysis/vv/provenance/neighbor_orientation_correlation_v2.md @@ -1,58 +1,67 @@ -# Exemplar Archive Provenance: neighbor_orientation_correlation_v2.tar.gz — RETIRED - -**Status: RETIRED 2026-07-06 (same day as creation, never consumed by a merged commit).** -No test consumes this archive; there is no `download_test_data()` entry for it. This sidecar -is retained as the forensic record of why the filter now has **no exemplar archive at all**. - ---- - -## Why the filter has no exemplar archive - -The V&V policy forbids circular oracles. Both generations of this filter's exemplar failed -that bar: - -- **v1 (`neighbor_orientation_correlation.tar.gz`, retired 2026-07-06):** its data container - was named `DataContainer` while the test looked up `Exemplar Data/...`, so every array - lookup returned null and the comparison loop's silent `continue` skipped everything — - **the exemplar comparison never compared a single array at any commit in its history.** Bisect proof (2026-07-07): the comparison loop was introduced already mapping to `Exemplar Data` with the silent `continue` (commit `d199bc749`, 2022-07-24, pre-simplnx plugin repo), and the archive's SHA512 (`1223674…`) is unchanged from its first `download_test_data()` registration (commit `e34baf1f2`, 2022-12-02) through retirement — its container was always `DataContainer`. Hollow from birth. -- **v2 (this archive):** created during the 2026-07-06 V&V cycle from the post-fix SIMPLNX - output as a regression pin. That is a circular oracle by construction; it was retired the - same day, before the V&V branch merged, and the `Small IN100 Pipeline` test was rewritten - to be archive-free. - -## What replaced it - -- **Correctness (non-circular):** 11 inline oracle fixtures in - `test/NeighborOrientationCorrelationTest.cpp` (`Oracle F01`–`F11`, `Class 4` invariants). - Toy data is built programmatically; expected values are hand-derived (Class 1, co-axial - z-rotation regime where every misorientation convention reduces to `|Δθ|`) and - cross-checked by a NumPy reference implementation (Class 2, `reference_noc.py`, - NumPy 2.4.2, 33 assertions in `check_derivations.py`) — both derived independently of the - SIMPLNX and legacy implementations. - - > **⚠ Evidence archival (open action):** `reference_noc.py`, `check_derivations.py`, and - > `DERIVATIONS.md` live only in the engineer's local `VV_Work/` folder and are **not** committed - > to the repository, so the Class 2 reference oracle is not reproducible from the repo. They must be - > uploaded to the OneDrive verification archive (per the archive-filter-verification workflow) and - > this note replaced with the archive link before final sign-off. Mitigation: every fixture's - > expected value is *also* hand-derivable (Class 1) and encoded inline in the test, so the fixtures - > remain auditable even while the reference scripts are unarchived. -- **Production scale:** the `Small IN100 Pipeline` test snapshots the full cell data before - the filter runs and asserts the Class 4 invariants at 4.4M cells (high-confidence cells - bit-identical across every array; every modified cell was low-confidence; at least one - cell modified). Invariants derive from the filter's specification, not from any - implementation's output. - -## Archive identity (historical) +# Exemplar Archive Provenance: neighbor_orientation_correlation_v2.tar.gz + +This output archive was retired on 2026-07-06. Current correctness checks use inline analytical fixtures and input snapshots. + +## Archive identity | Field | Value | |---|---| -| **Archive** | `neighbor_orientation_correlation_v2.tar.gz` | -| **SHA512** | `1596d028af1e885005eda9d07e118fc4a03afb4cf30064095052805ea0c098759e4968ceac2f82ac58eae1c9c92a367eae93b4bcd332cebf1622d06ea968e9e7` | -| **Used by tests** | none (retired) | -| **Generated / retired by** | Michael Jackson, 2026-07-06 | +| **Archive** | `neighbor_orientation_correlation_v2.tar.gz` (retired) | +| **SHA512** | `1596d028af1e885005eda9d07e118fc4a03afb4cf30064095052805ea0c098759e4968ceac2f82ac58eae1c9c92a367eae93b4bcd332cebf1622d06ea968e9e7` (retired output; no current registration) | +| **Used by tests** | None | +| **Generated by** | Michael Jackson | +| **Generated on** | 2026-07-06 | +| **Generated at commit** | Not applicable; the archive was retired before registration. PR #1655 (`71526a1f5`) contains the retirement record. | + +## How it was generated + +The archive was generated from SIMPLNX output after the pass-count and selection corrections. It was rejected as an independent oracle because the filter under test supplied the expected output. `Small IN100 Pipeline` instead snapshots its input and checks invariants; its input archive is documented in `NeighborOrientationCorrelationFilter_inputs.md`. + +The synthetic comparison fixtures are defined in `vv_work:NeighborOrientationCorrelationFilter/fixtures/make_fixtures.py` and `vv_work:NeighborOrientationCorrelationFilter/fixtures/manifest.json`. Pipelines are in `vv_work:NeighborOrientationCorrelationFilter/pipelines/`. These are separate from the retired production output archive. + +## Canonical oracle output + +No array in this retired archive is a canonical oracle output. The current test defines expectations in `src/Plugins/OrientationAnalysis/test/NeighborOrientationCorrelationTest.cpp`. + +| DataPath | Source of expected values | +|---|---| +| `DataContainer/CellData/{Confidence Index,Phases,Quats,SourceIndex,Payload2,Mask}` | Class 1 source-index derivations for F01–F13; input snapshots verify replaced and unchanged tuples. F04 and F10 assert the cascade confidence values, and F04 also asserts identity quaternions. | +| `DataContainer/CellData/{NeighborIds,Labels}` | Class 1 F13: only cell 62 copies cell 87. | +| Production numeric cell arrays | Class 4: high-confidence cells stay unchanged, and at least one low-confidence cell changes. | + +F08 in the current C++ test uses a 58° pair to distinguish hexagonal and cubic symmetry. The archived F08 reference fixture uses uniform hexagonal neighbors. The 12-fixture reference comparison does not replace this newer discriminating assertion. F13 checks NeighborList and String transfer separately from the 12 reference fixtures. + +## Oracle provenance (Classes 2, 3, 5 only) + +- **Library:** NumPy +- **Exact version:** 2.4.2, recorded in `vv_work:NeighborOrientationCorrelationFilter/oracle/DERIVATIONS.md`. +- **Script in archive:** `vv_work:NeighborOrientationCorrelationFilter/oracle/reference_noc.py`. +- **Derivations:** `vv_work:NeighborOrientationCorrelationFilter/oracle/check_derivations.py` and `vv_work:NeighborOrientationCorrelationFilter/oracle/DERIVATIONS.md`. +- **Random seed:** Not applicable; the 12 fixtures are deterministic. + +The reference selects the last neighbor among equal maximum positive counts. It uses co-axial rotations with analytical misorientations. `vv_work:NeighborOrientationCorrelationFilter/notes/ComparisonResults.md` records 35 successful hand-derivation checks. + +Run against DREAM.3D v6.5.171 on 2026-07-07: 7 of 12 fixture cases match the SIMPLNX version used in that run. F05 differs because of D1, F04 and F10 because of D2, and F03 and F12 because of D3. A local proof version with those corrections matches all 12. The runner identity appears in the comparison notes and `vv_work:NeighborOrientationCorrelationFilter/results/legacy/legacy_NOC_F01_replace_uniform_3d.legacy.log` (SIMPLib 1.2.828). + +The same dated record gives the Small IN100 comparison at confidence 0.2, tolerance 5° and Level 2. Of 4,444,713 cells, 635,114 differ: 115,380 change only in SIMPLNX, 22,834 only in DREAM.3D, and 496,900 receive different replacements among the 999,660 changed by both. SIMPLNX changes 1,115,040 cells; DREAM.3D changes 1,022,494. The local proof version matches that SIMPLNX output on every cell. This records zero observed precision effect on that input and those implementations. + +Those results apply to the 2026-07-07 algorithm. Later algorithm-file changes are `71526a1f5` (2026-07-10, V&V fixes), `4ec21e93f` (2026-09-20, slice-transfer rewrite), `a33c370ca` (2026-09-25, messaging API), and `62000012e` (2026-10-20, progress API). Current numeric transfer uses a slice snapshot and clears source marks after each slice. The archived reference instead models ascending in-place transfer and persistent source marks. The dated comparison therefore does not establish parity for the current algorithm. + +Release-source inspection covers `upstream/release/v7.0.0`, `v7.0.1`, `v7.0.2`, `v7.0.3`, `v7.2.0`, `v7.2.1`, `v7.2.2`, `v7.4.0`, `v7.4.1` and `v7.4.2`. In `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/NeighborOrientationCorrelation.cpp`, all ten avoid D1; the first nine contain D2, D3 and the numeric-only transfer list (D6); v7.4.2 contains their corrections. All ten omit the phase and Laue bounds checks (D7). Their test sources contain the skipped exemplar comparison through v7.4.1 and snapshot invariants in v7.4.2 (D5). + +DREAM.3D tag `v6.5.171`, `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/NeighborOrientationCorrelation.cpp`, contains D1, D2, D3 and the unchecked indices of D7. It copies all cell arrays, so it does not have D6. D5 belongs to the SIMPLNX test, not the legacy filter. PR #1655 (`71526a1f5`) contains D2, D3, D5 and D6 corrections; PR #1568 (`4ec21e93f`) contains the D7 checks and D8 transfer changes. + +The 19 ordinary cases form the in-core CI validation set on Linux, macOS and Windows. All pass in CI at `419f9db35`. The hidden `Generate Test Data` case writes input and is not a correctness test. ## Second-engineer oracle review - **Reviewer:** Michael Jackson (technical authority) - **Date:** 2026-07-16 + +This is the existing V&V sign-off. It names no separate second engineer. + +## Regenerated to fix a circular-oracle situation? + +The v2 output was created as a replacement for `neighbor_orientation_correlation.tar.gz`, then retired because SIMPLNX itself supplied its values. No replacement output archive is used. The v1 test changed the container name to `Exemplar Data` and skipped missing arrays, while the output file used `DataContainer`; see `vv_work:NeighborOrientationCorrelationFilter/notes/ComparisonResults.md` and `NeighborOrientationCorrelationFilter-D5`. + +The v1 SHA512 registered before retirement was `122367452174ade2f24dde7a4610bddc4f147a223722d9b30c1df9eaa2cd2bf25e1c7957aba83f3f9de79b4eadd79339b848f9530d1ebf44c69244ea5442cf85`. Neither filter-output archive is registered in the current `src/Plugins/OrientationAnalysis/test/CMakeLists.txt`. diff --git a/src/Plugins/SimplnxCore/docs/IdentifySampleFilter.md b/src/Plugins/SimplnxCore/docs/IdentifySampleFilter.md index 7164db69be..6baabde081 100644 --- a/src/Plugins/SimplnxCore/docs/IdentifySampleFilter.md +++ b/src/Plugins/SimplnxCore/docs/IdentifySampleFilter.md @@ -17,8 +17,8 @@ The algorithm is: If *Fill Holes* is enabled, two additional steps are run: -1. Search for the largest contiguous set of *bad* cells. This is assumed to be the outer border region. -2. Change all other *bad* cells to *good*. (This removes the "speckling" of *bad* cells inside the sample.) +1. Identify contiguous sets of *bad* cells that touch the image boundary. Axes with only one cell do not count as boundaries; a single-cell image remains unchanged. +2. Change all *bad* cells in enclosed sets to *good*. (This removes the "speckling" of *bad* cells inside the sample.) *Note:* If the sample contains real holes, enabling *Fill Holes* will close them by calling all cells "inside" the sample *good*. To reidentify those holes afterward, re-run the threshold filter with the criteria *GoodVoxels = 1* AND whatever original criterion identified the holes. This limits the original hole-finding criteria to within the sample and not the outer border region. diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeTriangleGeomCentroids.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeTriangleGeomCentroids.cpp index 350a3116ad..c07ff09e4a 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeTriangleGeomCentroids.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeTriangleGeomCentroids.cpp @@ -6,6 +6,8 @@ #include "simplnx/DataStructure/Geometry/TriangleGeom.hpp" #include "simplnx/Utilities/GeometryHelpers.hpp" +#include + using namespace nx::core; // ----------------------------------------------------------------------------- @@ -40,15 +42,15 @@ Result<> ComputeTriangleGeomCentroids::operator()() IGeometry::MeshIndexType numTriangles = triangleGeom.getNumberOfFaces(); const BoundingBox3Df boundingBox = triangleGeom.getBoundingBox(); - // Get the faceLabels array and then get the min and max values + // Preflight ensures Face Labels is nonempty and has one tuple per triangle. const Int32AbstractDataStore& faceLabels = m_DataStructure.getDataAs(m_InputValues->FaceLabelsArrayPath)->getDataStoreRef(); - auto [minFeatureId, maxFeatureId] = std::minmax_element(faceLabels.begin(), faceLabels.end()); + const int64 maxFeatureId = *std::max_element(faceLabels.begin(), faceLabels.end()); auto& featAttrMat = m_DataStructure.getDataRefAs(m_InputValues->FeatureAttributeMatrixPath); - if(featAttrMat.getNumberOfTuples() < *maxFeatureId + 1) + if(maxFeatureId > 0 && featAttrMat.getNumberOfTuples() < static_cast(maxFeatureId + 1)) { m_MessageHandler.sendMessage(IFilter::Message::Type::Info, "Increasing Number of tuples in target feature attribute matrix..."); - Result<> resizeResult = featAttrMat.resizeTuples(ShapeType{static_cast(*maxFeatureId + 1)}); + Result<> resizeResult = featAttrMat.resizeTuples(ShapeType{static_cast(maxFeatureId + 1)}); if(resizeResult.invalid()) { return resizeResult; @@ -93,17 +95,19 @@ Result<> ComputeTriangleGeomCentroids::operator()() const std::set& vertexSet = vertexSets[i]; auto periodicFaces = GeometryHelpers::Topology::FindElementPeriodicFaces(boundingBox, vertexCoords, vertexSet); + std::array coordinateSums = {}; for(const auto& vert : vertexSets[i]) { - centroids[3 * i + 0] += vertexCoords[3 * vert + 0]; - centroids[3 * i + 1] += vertexCoords[3 * vert + 1]; - centroids[3 * i + 2] += vertexCoords[3 * vert + 2]; + coordinateSums[0] += vertexCoords[3 * vert + 0]; + coordinateSums[1] += vertexCoords[3 * vert + 1]; + coordinateSums[2] += vertexCoords[3 * vert + 2]; } if(!vertexSets[i].empty()) { - centroids[3 * i + 0] /= static_cast(vertexSets[i].size()); - centroids[3 * i + 1] /= static_cast(vertexSets[i].size()); - centroids[3 * i + 2] /= static_cast(vertexSets[i].size()); + const float64 numVertices = static_cast(vertexSets[i].size()); + centroids[3 * i + 0] = static_cast(coordinateSums[0] / numVertices); + centroids[3 * i + 1] = static_cast(coordinateSums[1] / numVertices); + centroids[3 * i + 2] = static_cast(coordinateSums[2] / numVertices); if(m_InputValues->IsPeriodic) { diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/IdentifySampleCCL.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/IdentifySampleCCL.cpp index d2c33780a4..9cd5b2bbff 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/IdentifySampleCCL.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/IdentifySampleCCL.cpp @@ -973,7 +973,9 @@ struct IdentifySampleCCLFunctor auto boundaryResult = replayForwardCCL( goodVoxels, dimX, dimY, dimZ, *holeCCL.value().equivalences, holeCondition, [&boundaryRoots, dimX, dimY, dimZ, &shouldCancel](T* /*data*/, usize /*inSlice*/, uint64 root, usize x, usize y, usize z) -> Result { - if(x == 0 || x == static_cast(dimX - 1) || y == 0 || y == static_cast(dimY - 1) || z == 0 || z == static_cast(dimZ - 1)) + // Ignore collapsed axes, as BFS does, but keep a lone bad voxel outside hole filling. + if((dimX == 1 && dimY == 1 && dimZ == 1) || (dimX > 1 && (x == 0 || x == static_cast(dimX - 1))) || (dimY > 1 && (y == 0 || y == static_cast(dimY - 1))) || + (dimZ > 1 && (z == 0 || z == static_cast(dimZ - 1)))) { auto markResult = boundaryRoots->mark(root, shouldCancel); if(markResult.invalid()) diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeTriangleGeomCentroidsFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeTriangleGeomCentroidsFilter.cpp index 87f030198c..ab189f6624 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeTriangleGeomCentroidsFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeTriangleGeomCentroidsFilter.cpp @@ -4,12 +4,14 @@ #include "simplnx/DataStructure/AttributeMatrix.hpp" #include "simplnx/DataStructure/DataPath.hpp" +#include "simplnx/DataStructure/Geometry/TriangleGeom.hpp" #include "simplnx/Filter/Actions/CreateArrayAction.hpp" #include "simplnx/Parameters/ArraySelectionParameter.hpp" #include "simplnx/Parameters/AttributeMatrixSelectionParameter.hpp" #include "simplnx/Parameters/BoolParameter.hpp" #include "simplnx/Parameters/DataObjectNameParameter.hpp" #include "simplnx/Parameters/GeometrySelectionParameter.hpp" +#include "simplnx/Utilities/DataArrayUtilities.hpp" using namespace nx::core; @@ -85,6 +87,20 @@ IFilter::PreflightResult ComputeTriangleGeomCentroidsFilter::preflightImpl(const { auto pFaceLabelsArrayPath = filterArgs.value(k_FaceLabelsArrayPath_Key); auto pFeatureAttributeMatrixPath = filterArgs.value(k_FeatureAttributeMatrixPath_Key); + auto pTriangleGeometryPath = filterArgs.value(k_TriGeometryDataPath_Key); + + const auto& triangleGeom = dataStructure.getDataRefAs(pTriangleGeometryPath); + const auto& faceLabels = dataStructure.getDataRefAs(pFaceLabelsArrayPath); + if(faceLabels.getNumberOfTuples() == 0) + { + return MakePreflightErrorResult(-12903, fmt::format("Face Labels array '{}' has 0 tuples. At least 1 face label tuple is required to compute feature centroids. Select a nonempty triangle mesh.", + pFaceLabelsArrayPath.toString())); + } + if(triangleGeom.getNumberOfFaces() == 0 || !CheckArraysHaveSameTupleCount(dataStructure, {pFaceLabelsArrayPath, triangleGeom.getFaces()->getDataPaths().front()})) + { + return MakePreflightErrorResult(-12902, fmt::format("Face Labels array '{}' has {} tuples, but Triangle Geometry '{}' has {} faces. Select an array with one tuple per face.", + pFaceLabelsArrayPath.toString(), faceLabels.getNumberOfTuples(), pTriangleGeometryPath.toString(), triangleGeom.getNumberOfFaces())); + } PreflightResult preflightResult; diff --git a/src/Plugins/SimplnxCore/test/ComputeGroupingDensityTest.cpp b/src/Plugins/SimplnxCore/test/ComputeGroupingDensityTest.cpp index 06047e4eca..9b63c87b11 100644 --- a/src/Plugins/SimplnxCore/test/ComputeGroupingDensityTest.cpp +++ b/src/Plugins/SimplnxCore/test/ComputeGroupingDensityTest.cpp @@ -61,7 +61,7 @@ constexpr usize k_NumParents = 3; // // The v2 exemplar archive was hand-reviewed and signed off by the filter author, // and the SIMPLNX outputs in it were independently confirmed bit-identical to -// the legacy DREAM3D 6.5.172 `FindGroupingDensity` filter (the pre-SIMPLNX port +// the `FindGroupingDensity` filter in a locally patched legacy build (the pre-SIMPLNX port // source) — see src/Plugins/SimplnxCore/vv/ComputeGroupingDensityFilter.md and // src/Plugins/SimplnxCore/vv/deviations/ComputeGroupingDensityFilter.md. // diff --git a/src/Plugins/SimplnxCore/test/ComputeTriangleGeomCentroidsTest.cpp b/src/Plugins/SimplnxCore/test/ComputeTriangleGeomCentroidsTest.cpp index 5dacf0517c..f1c7b2a00b 100644 --- a/src/Plugins/SimplnxCore/test/ComputeTriangleGeomCentroidsTest.cpp +++ b/src/Plugins/SimplnxCore/test/ComputeTriangleGeomCentroidsTest.cpp @@ -11,6 +11,8 @@ #include +#include + using namespace nx::core; using namespace nx::core::UnitTest; @@ -67,10 +69,10 @@ TEST_CASE("SimplnxCore::ComputeTriangleGeomCentroids", "[SimplnxCore][ComputeTri const DataPath kExemplarArrayPath = k_FeatureAttributeMatrixPath.createChildPath(kExemplarArrayName); const DataPath kNxArrayPath = k_FeatureAttributeMatrixPath.createChildPath(k_CentroidsArrayName); - const auto& kExemplarsArray = dataStructure.getDataRefAs(kExemplarArrayPath); - const auto& kNxArray = dataStructure.getDataRefAs(kNxArrayPath); - - CompareDataArrays(kExemplarsArray, kNxArray); + // The archive matches legacy float32 accumulation exactly. Float64 accumulation changes its + // centroids by at most 0.041534423828125 (feature 518, X). Keep the archive and allow 0.05 absolute + // error here; the Large Coordinate Analytical Mean test checks the corrected precision exactly. + CompareFloatArraysWithNans(dataStructure, kExemplarArrayPath, kNxArrayPath, 0.05F); } #ifdef SIMPLNX_WRITE_TEST_OUTPUT @@ -82,6 +84,17 @@ TEST_CASE("SimplnxCore::ComputeTriangleGeomCentroids", "[SimplnxCore][ComputeTri namespace { +Arguments CentroidArguments(bool isPeriodic = false) +{ + Arguments args; + args.insertOrAssign(ComputeTriangleGeomCentroidsFilter::k_TriGeometryDataPath_Key, std::make_any(k_GeometryPath)); + args.insertOrAssign(ComputeTriangleGeomCentroidsFilter::k_FaceLabelsArrayPath_Key, std::make_any(k_FaceLabelsPath)); + args.insertOrAssign(ComputeTriangleGeomCentroidsFilter::k_FeatureAttributeMatrixPath_Key, std::make_any(k_FeatureAttributeMatrixPath)); + args.insertOrAssign(ComputeTriangleGeomCentroidsFilter::k_CentroidsArrayName_Key, std::make_any(k_CentroidsArrayName)); + args.insertOrAssign(ComputeTriangleGeomCentroidsFilter::k_IsPeriodic_Key, std::make_any(isPeriodic)); + return args; +} + // Builds a tiny triangle mesh on a periodic domain X in [0, 4]. All vertices sit at z = 0 and each feature // occupies a distinct y value, so ONLY the X axis can trip the periodic (both-opposing-faces) condition; // the whole-geometry bounding box is exactly x=[0,4], y=[1,3], z=[0,0]. Face labels group triangles into @@ -219,3 +232,124 @@ TEST_CASE("SimplnxCore::ComputeTriangleGeomCentroids: Periodic Minimum-Image Ora UnitTest::CheckArraysInheritTupleDims(dataStructure); } } + +TEST_CASE("SimplnxCore::ComputeTriangleGeomCentroids: Face Labels Tuple Count", "[SimplnxCore][ComputeTriangleGeomCentroids]") +{ + UnitTest::LoadPlugins(); + DataStructure dataStructure = BuildPeriodicToyMesh(); + const usize labelTuples = GENERATE(usize{7}, usize{9}); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_FaceLabelsPath.getParent())); + { + const auto resizeResult = dataStructure.getDataRefAs(k_FaceLabelsPath.getParent()).resizeTuples({labelTuples}); + SIMPLNX_RESULT_REQUIRE_VALID(resizeResult); + } + + ComputeTriangleGeomCentroidsFilter filter; + const auto result = filter.preflight(dataStructure, CentroidArguments()); + SIMPLNX_RESULT_REQUIRE_INVALID(result.outputActions); + REQUIRE(result.outputActions.errors()[0].code == -12902); + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} + +TEST_CASE("SimplnxCore::ComputeTriangleGeomCentroids: Empty Face Labels", "[SimplnxCore][ComputeTriangleGeomCentroids]") +{ + UnitTest::LoadPlugins(); + DataStructure dataStructure = BuildPeriodicToyMesh(); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_GeometryPath)); + auto& geometry = dataStructure.getDataRefAs(k_GeometryPath); + { + const auto resizeResult = geometry.getFaceAttributeMatrix()->resizeTuples({0}); + SIMPLNX_RESULT_REQUIRE_VALID(resizeResult); + } + { + const auto resizeResult = geometry.getVertexAttributeMatrix()->resizeTuples({0}); + SIMPLNX_RESULT_REQUIRE_VALID(resizeResult); + } + REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_FaceLabelsPath.getParent())); + { + const auto resizeResult = dataStructure.getDataRefAs(k_FaceLabelsPath.getParent()).resizeTuples({0}); + SIMPLNX_RESULT_REQUIRE_VALID(resizeResult); + } + + ComputeTriangleGeomCentroidsFilter filter; + const auto result = filter.preflight(dataStructure, CentroidArguments()); + SIMPLNX_RESULT_REQUIRE_INVALID(result.outputActions); + REQUIRE(result.outputActions.errors()[0].code == -12903); + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} + +TEST_CASE("SimplnxCore::ComputeTriangleGeomCentroids: Nonpositive Face Labels", "[SimplnxCore][ComputeTriangleGeomCentroids]") +{ + UnitTest::LoadPlugins(); + DataStructure dataStructure = BuildPeriodicToyMesh(); + const int32 label = GENERATE(int32{-2}, std::numeric_limits::min(), int32{-1}, int32{0}); + const bool isPeriodic = GENERATE(false, true); + CAPTURE(label, isPeriodic); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_FaceLabelsPath)); + dataStructure.getDataRefAs(k_FaceLabelsPath).fill(label); + + ComputeTriangleGeomCentroidsFilter filter; + const auto result = filter.execute(dataStructure, CentroidArguments(isPeriodic)); + SIMPLNX_RESULT_REQUIRE_VALID(result.result); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_FeatureAttributeMatrixPath)); + REQUIRE(dataStructure.getDataRefAs(k_FeatureAttributeMatrixPath).getNumberOfTuples() == 5); + const DataPath centroidsPath = k_FeatureAttributeMatrixPath.createChildPath(k_CentroidsArrayName); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(centroidsPath)); + const auto& centroids = dataStructure.getDataRefAs(centroidsPath); + for(usize index = 0; index < centroids.getSize(); index++) + { + REQUIRE(centroids[index] == 0.0F); + } + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} + +TEST_CASE("SimplnxCore::ComputeTriangleGeomCentroids: Large Coordinate Analytical Mean", "[SimplnxCore][ComputeTriangleGeomCentroids]") +{ + UnitTest::LoadPlugins(); + DataStructure dataStructure = BuildPeriodicToyMesh(); + constexpr usize k_NumFaces = 1000; + constexpr usize k_NumVertices = 3 * k_NumFaces; + constexpr float32 k_Offset = 100000000.0F; + REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_GeometryPath)); + auto& geometry = dataStructure.getDataRefAs(k_GeometryPath); + { + const auto resizeResult = geometry.getVertexAttributeMatrix()->resizeTuples({k_NumVertices}); + SIMPLNX_RESULT_REQUIRE_VALID(resizeResult); + } + { + const auto resizeResult = geometry.getFaceAttributeMatrix()->resizeTuples({k_NumFaces}); + SIMPLNX_RESULT_REQUIRE_VALID(resizeResult); + } + REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_FaceLabelsPath.getParent())); + { + const auto resizeResult = dataStructure.getDataRefAs(k_FaceLabelsPath.getParent()).resizeTuples({k_NumFaces}); + SIMPLNX_RESULT_REQUIRE_VALID(resizeResult); + } + REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_FaceLabelsPath)); + auto& labels = dataStructure.getDataRefAs(k_FaceLabelsPath); + for(usize vertex = 0; vertex < k_NumVertices; vertex++) + { + (*geometry.getVertices())[3 * vertex] = k_Offset + (vertex % 3 == 1 ? 8.0F : 0.0F); + (*geometry.getVertices())[3 * vertex + 1] = -k_Offset + (vertex % 3 == 2 ? 8.0F : 0.0F); + (*geometry.getVertices())[3 * vertex + 2] = 64.0F; + (*geometry.getFaces())[vertex] = vertex; + } + for(usize face = 0; face < k_NumFaces; face++) + { + labels[2 * face] = 1; + labels[2 * face + 1] = -1; + } + + ComputeTriangleGeomCentroidsFilter filter; + const auto result = filter.execute(dataStructure, CentroidArguments()); + SIMPLNX_RESULT_REQUIRE_VALID(result.result); + const DataPath centroidsPath = k_FeatureAttributeMatrixPath.createChildPath(k_CentroidsArrayName); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(centroidsPath)); + const auto& centroids = dataStructure.getDataRefAs(centroidsPath); + // Each coordinate has 1000 identical triples. Exact means are offset + 8/3, -offset + 8/3, and 64. + // At this offset float32 spacing is 8, so the correctly rounded first two means equal +/-offset. + REQUIRE(centroids[3] == k_Offset); + REQUIRE(centroids[4] == -k_Offset); + REQUIRE(centroids[5] == 64.0F); + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} diff --git a/src/Plugins/SimplnxCore/test/IdentifySampleTest.cpp b/src/Plugins/SimplnxCore/test/IdentifySampleTest.cpp index ba4e199ff1..3407098c36 100644 --- a/src/Plugins/SimplnxCore/test/IdentifySampleTest.cpp +++ b/src/Plugins/SimplnxCore/test/IdentifySampleTest.cpp @@ -188,6 +188,55 @@ TEST_CASE("SimplnxCore::IdentifySampleFilter", "[SimplnxCore][IdentifySampleFilt } } +TEMPLATE_TEST_CASE("SimplnxCore::IdentifySampleFilter: unit dimensions hole filling", "[SimplnxCore][IdentifySampleFilter]", bool, uint8) +{ + UnitTest::LoadPlugins(); + const auto scenario = GENERATE(from_range(UnitTest::SelectAlgorithmTestScenariosForInMemoryStores())); + const auto dims = GENERATE(SizeVec3{5, 5, 1}, SizeVec3{5, 1, 5}, SizeVec3{1, 5, 5}, SizeVec3{1, 1, 1}); + const bool fillHoles = GENERATE(false, true); + CAPTURE(scenario, dims[0], dims[1], dims[2], fillHoles); + UnitTest::AlgorithmTestScope scope(scenario); + + DataStructure dataStructure; + auto* geometry = ImageGeom::Create(dataStructure, "Image"); + REQUIRE(geometry != nullptr); + geometry->setDimensions(dims); + geometry->setSpacing({1.0F, 1.0F, 1.0F}); + geometry->setOrigin({0.0F, 0.0F, 0.0F}); + const ShapeType shape{dims[2], dims[1], dims[0]}; + auto* cellData = AttributeMatrix::Create(dataStructure, "CellData", shape, geometry->getId()); + REQUIRE(cellData != nullptr); + geometry->setCellData(*cellData); + auto store = DataStoreUtilities::CreateDataStore(dataStructure, k_NonSquareMaskPath, shape, {1}); + auto* mask = DataArray::Create(dataStructure, "Mask", store, cellData->getId()); + REQUIRE(mask != nullptr); + + // Each 5 by 5 plane has one enclosed center hole and one bad corner. + // The corner (or the sole voxel in a 1 by 1 by 1 image) must stay bad. + mask->fill(static_cast(true)); + mask->setValue(0, static_cast(false)); + if(mask->getNumberOfTuples() > 1) + { + mask->setValue(12, static_cast(false)); + } + + IdentifySampleFilter filter; + auto args = CreateNonSquareArguments(); + args.insertOrAssign(IdentifySampleFilter::k_FillHoles_Key, std::make_any(fillHoles)); + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + auto executeResult = scope.executeFilter(filter, dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + + for(usize i = 0; i < mask->getNumberOfTuples(); i++) + { + CAPTURE(i); + const bool expected = i != 0 && (i != 12 || fillHoles); + CHECK(mask->getValue(i) == static_cast(expected)); + } + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} + TEST_CASE("SimplnxCore::IdentifySampleFilter: genuine HDF5 equivalence-page and slice oracle", "[SimplnxCore][IdentifySampleFilter][.OocStoreContract]") { UnitTest::LoadPlugins(); diff --git a/src/Plugins/SimplnxCore/vv/ComputeFeatureCentroidsFilter.md b/src/Plugins/SimplnxCore/vv/ComputeFeatureCentroidsFilter.md index bf0e7881cf..607bc4b94d 100644 --- a/src/Plugins/SimplnxCore/vv/ComputeFeatureCentroidsFilter.md +++ b/src/Plugins/SimplnxCore/vv/ComputeFeatureCentroidsFilter.md @@ -1,165 +1,118 @@ # V&V Report: ComputeFeatureCentroidsFilter -| | | -|-----------|------------------| -| Plugin | SimplnxCore | +| | | +|--------|--------------| +| Plugin | SimplnxCore | | SIMPLNX UUID | `c6875ac7-8bdd-4f69-b6ce-82ac09bd3421` | | SIMPLNX Human Name | Compute Feature Centroids | -| DREAM3D 6.5.171 equivalent | `FindFeatureCentroids` (SIMPL UUID `6f8ca36f-2995-5bd3-8672-6b0b80d5b2ca`) — `Source/Plugins/Generic/GenericFilters/FindFeatureCentroids.{h,cpp}` | +| DREAM.3D v6.5.171 equivalent | `FindFeatureCentroids` (Generic plugin, SIMPL UUID `6f8ca36f-2995-5bd3-8672-6b0b80d5b2ca`) | | Verified commit | ** | -| Status | COMPLETE — 2026-07-16 | -| Sign-off | Michael Jackson — 2026-07-16 | +| Status | COMPLETE | +| Sign-off | Michael Jackson, 2026-07-07; second engineer: Matthew Marine, 2026-07-10 | ## At a glance -| Aspect | Current state | -|------------------------|--------------------------| -| Algorithm Relationship | **Port** — the non-periodic path of `ComputeFeatureCentroids::operator()` is a near line-by-line translation of legacy `FindFeatureCentroids::find_centroids()`, including the identical Kahan compensated summation and `float64`/`float64`/`uint64` sum/compensation/count triplet. Port-time additions: SIMPLNX-only `Is Periodic` option, disabled `ParallelDataAlgorithm` scaffolding, `float64` voxel-center fetch (`Point3Dd`), and a FeatureIds→AttributeMatrix indexing guard. | -| Oracle (confirmed) | **Class 1 (Analytical) primary + Class 4 (Invariant) companion** (confirmed 2026-07-07). 6 hand-derived toy fixtures A–F cover arithmetic centroids, minimum-image periodic centroids, and the domain-filling fallback. Signed off by Michael Jackson (technical authority) 2026-07-16. | -| Code paths enumerated | 9 of 9 listed numerical/validation paths exercised; cancellation and internal error-site exhaustiveness are outside this campaign. | -| Tests today | 5 registered cases plus 1 hidden real-HDF5 block-tail case, which runs periodic mode off and on. Serial CTest passes 5/5 in both builds. | -| Exemplar archive | **None — inline analytical fixtures** (provenance sidecar written). The shared `6_6_stats_test_v2.tar.gz` is no longer consumed by this test (kept for 5 other tests); `6_6_find_feature_centroids.tar.gz` kept (used by ExtractComponentAsArray / WriteAbaqusHexahedron). | -| Legacy comparison | **Source-inspection** (DREAM3D 6.5.171 vs SIMPLNX; local legacy source used for corroboration), backed by the independent Class 1 fixtures. Non-periodic path is an exact Kahan port; 2 deviations (D1 float32→float64 voxel-center precision; D2 SIMPLNX-only `Is Periodic`). Empirical binary A/B available if bit-confirmation is required. | -| Bug flags | **None outstanding.** Two SIMPLNX-internal periodic-centroid defects were resolved: the original offset ignored spacing, and the constant half-domain offset model was incorrect for asymmetric wrapped features. The feature-centroid path now uses a minimum-image circular mean; the triangle-centroid utility uses a largest-empty-gap minimum-image mean. Fixtures D/E/F pin the corrected behavior. These are not legacy deviations because legacy has no periodic path. | -| V&V phase | Historical COMPLETE status and sign-off retained. OOC recertification verifies preserved analytical tests and a real-HDF5 block-tail case. | - -## Follow-up (2026-07, issue #1665) — periodic model reimplemented - -The original V&V (#1658) fixed a **spacing** sub-bug in the periodic path but validated the periodic result against the **constant half-domain offset model itself** (Fixture D's expected value was "naive + (dim−1)/2", a circular oracle — the test was designed around the code). An adversarial review (#1665) showed that model is fundamentally wrong: a constant offset is correct only when a wrapped feature's mass is symmetric about the seam, and for asymmetric features it places the centroid in the empty middle of the feature or outside the domain entirely. - -This follow-up (on top of merged #1658) corrects it: - -- **Periodic path reimplemented as a minimum-image circular mean.** During the existing cell sweep, per feature and axis, the unit vectors of each cell center's angular position around the domain are accumulated (`sumCos`, `sumSin`). At finalize, for any axis on which the feature spans the full extent, the centroid component becomes `atan2(sumSin, sumCos)` mapped back into the domain. A near-zero resultant (domain-filling feature) falls back to the arithmetic mean. O(numFeatures) extra memory; trig only when `Is Periodic` is on. -- **The range-based `ImageGeom` overload of `AdjustCentroidsForPeriodicFaces` was removed** — it received only per-feature index ranges, which are mathematically insufficient to compute a wrapped centroid. Nothing else called it. -- **Fixtures D and E were re-derived against an independent minimum-image oracle** (no longer circular): D → x = 3.5 (was 3.667), E (now asymmetric, non-unit spacing) → x = 11.0 (was 17.0). New Fixture F pins the domain-filling arithmetic-mean fallback. All pass in-core and out-of-core. -- The earlier conclusion that the sibling `BoundingBox` overload was sound is **superseded**: that overload carried the same constant-offset defect and was fixed under issue #1665 in the same branch as this follow-up. - -The non-periodic path is unchanged (still the verified Kahan port). Deviation `ComputeFeatureCentroidsFilter-D2` is updated accordingly. +| Aspect | Current state | +|--------|---------------| +| Algorithm Relationship | Port of the non-periodic `FindFeatureCentroids` calculation in DREAM.3D v6.5.171. SIMPLNX adds periodic centroids, double-precision cell coordinates and bulk data access. | +| Oracle (confirmed) | Class 1 analytical means and Class 4 invariants are encoded in `test/ComputeFeatureCentroidsTest.cpp`. All pass in CI. | +| Code paths enumerated | 17 of 27 exercised by the six ordinary test cases. The table identifies validation, cancellation, I/O and periodic-axis gaps. | +| Tests today | 7 test cases: analytical centroids, periodic centroids, invalid IDs, bulk reads, SIMPL conversion, cell tuple counts and a hidden storage-contract fixture. | +| Exemplar archive | None. The tests construct inputs and expected values in C++. | +| Legacy comparison | Run against DREAM.3D v6.5.171 on 2026-10-22: 1 of 3 cases match; the differences come from `ComputeFeatureCentroidsFilter-D1` and `ComputeFeatureCentroidsFilter-D2`. A local proof version with the D1 fix matches DREAM3D-NX in 2 of 3 cases; the remaining case uses the DREAM3D-NX-only periodic option (D2). | +| Bug flags | None. | +| V&V phase | COMPLETE. | ## Summary -`ComputeFeatureCentroidsFilter` computes the centroid of each feature as the average X/Y/Z position of all -cells belonging to that feature, using Kahan compensated summation for numerical precision. An optional -`Is Periodic` mode uses a minimum-image circular mean for features that span the image boundary. Verification -used a **Class 1 (Analytical)** oracle — arithmetic means for non-periodic features and independently derived -minimum-image means for wrapped features — paired with **Class 4 (Invariant)** bounding-box, fire-condition, -and in-domain predicates. SIMPLNX matched the analytical values on every fixture; the legacy diff reduces to -a single float32-vs-float64 precision non-deviation plus the SIMPLNX-only periodic feature, and the -reconciliation surfaced and fixed two SIMPLNX-internal periodic-centroid defects. +`ComputeFeatureCentroidsFilter` calculates each feature's mean cell-center position with compensated summation. Its optional periodic mode uses a circular mean on axes where the feature spans the image. Analytical fixtures and invariants verify the results; the V&V found and fixed two periodic-centroid defects. ## Algorithm Relationship -*Classification:* **Port** - -*Evidence:* `ComputeFeatureCentroids::operator()` is a near line-by-line translation of legacy -`FindFeatureCentroids::find_centroids()` (local legacy source `Generic/GenericFilters/FindFeatureCentroids.cpp:144–224`), -preserving the identical Kahan compensated summation, the `float64`/`float64`/`uint64` sum/compensation/count -triplet, and the `count>0` divide-and-store finalize. Same SIMPL UUID retained -(`6f8ca36f-2995-5bd3-8672-6b0b80d5b2ca`); SIMPL 6.4/6.5 conversion fixtures at -`test/simpl_conversion/6_*/ComputeFeatureCentroidsFilter.json`. - -*Port-time deltas:* +*Classification:* Port -1. **`Is Periodic` option** (SIMPLNX-only) + per-feature index-range tracking and unit-vector - accumulation for a minimum-image circular mean. Changes output when enabled; default `false` reproduces - legacy exactly. Legacy has no equivalent. Drives deviation D2. -2. **Voxel-center precision**: SIMPLNX fetches the voxel center directly in `float64` - (`ImageGeom::getCoords` → `Point3Dd`); legacy fetches into a `float[3]` then promotes to `double` inside the - Kahan step. Both store the final centroid as `float32`. Sub-ULP-at-float32 numeric delta. Drives D1. -3. **`ParallelDataAlgorithm` scaffolding present but `setParallelizationEnabled(false)`** — runs serially, same - iteration order as legacy. No output impact. -4. **`ValidateFeatureIdsToFeatureAttributeMatrixIndexing` guard** before the sweep — adds an error path legacy - lacked. No output impact on valid data. - -*Material PRs since baseline (2025-10-01):* none change the algorithm. #1547 (doc: `Is Periodic` description), -#1543 (doc: pipeline rename), #1588 (test: SIMPL 6.4/6.5 backwards-compat + 2 fixtures). +*Evidence:* The non-periodic calculation preserves the Kahan sums and division by cell count in `Source/Plugins/Generic/GenericFilters/FindFeatureCentroids.cpp`; SIMPLNX adds double-precision coordinates, periodic means, validation and bulk reads. ## Oracle -*Class:* **1 (Analytical)** primary + **4 (Invariant)** companion. (Confirmed by developer 2026-07-07.) -Class 3 N/A — no published paper for "mean position of the cells in a feature". +*Class:* 1 (Analytical), with 4 (Invariant) companions. -*Applied:* Each non-periodic centroid component is `Σ(voxel-center coord) / N` over the feature's -cells, where `voxel-center = origin + (index + 0.5)·spacing`. A wrapped component is the minimum-image -circular mean of those coordinates. Six fixtures cover the code paths: **A** (3×1×1, basic mean + -`count==0` background), **B** (4×2×1, multi-feature + single-cell + empty id), **C** (2×2×2, 3D z-stride), -**D** (4×1×1, asymmetric periodic wrap at unit spacing), **E** (4×1×1, asymmetric periodic wrap at non-unit -spacing and nonzero origin), and **F** (domain-filling distribution, which triggers the arithmetic-mean -fallback). Class 4 invariants assert that `count==0 ⇒ centroid == (0,0,0)`, non-periodic components remain -inside their feature's voxel-center bounding box, periodic adjustment fires **iff** the feature spans the full -extent on that axis, and adjusted centroids remain inside the periodic domain. +*Applied:* Hand-derived arithmetic means check ordinary features; independently unwrapped coordinates check periodic fixtures D and E, while fixture F checks the arithmetic fallback and fixture D checks an unchanged non-spanning feature. -*Encoded:* `test/ComputeFeatureCentroidsTest.cpp` (`namespace CentroidToy`) — `Class 1 - Analytical Centroids` -(Fixtures A/B/C) and `Class 1/4 - Periodic Boundary` (Fixtures D/E/F), built + run in `NX-Com-Qt69-Vtk96-Rel`, -all pass at float32 margin 1e-4. +*Encoded:* `src/Plugins/SimplnxCore/test/ComputeFeatureCentroidsTest.cpp::Class 1 - Analytical Centroids` and `::Class 1/4 - Periodic Boundary` contain six fixtures A–F with absolute margin `1e-4`; `::Bulk boundary and failure propagation` adds a 65,537-cell analytical fixture. All pass in CI. -*Second-engineer review:* **Signed off by Michael Jackson (technical authority), 2026-07-16.** +*Second-engineer review:* Michael Jackson, 2026-07-16 (recorded technical-authority review). ## Bugs found and fixed -No new defect was found during this OOC recertification. The historical periodic-spacing correction described above remains in the branch. +All defects in this table are fixed. DREAM3D-NX v7.4.2 contains the fix for `ComputeFeatureCentroidsFilter-D3`. The fix for `ComputeFeatureCentroidsFilter-D4` will be in the DREAM3D-NX release after v7.4.2. + +| Deviation | Defect | Affected released versions | Resolution | +|-----------|--------|----------------------------|------------| +| `ComputeFeatureCentroidsFilter-D3` | The periodic offset ignored spacing and mixed cell-index distances with physical coordinates. | DREAM3D-NX v7.2.1 through v7.4.1. DREAM.3D v6.5.171 was not affected. | Periodic calculations use physical coordinates and domain lengths that include spacing. | +| `ComputeFeatureCentroidsFilter-D4` | A constant half-span offset gave incorrect centroids for asymmetric wrapped features and could place them outside the domain. | DREAM3D-NX v7.2.1 through v7.4.2. DREAM.3D v6.5.171 was not affected. | A circular mean replaces the offset; a near-zero resultant preserves the arithmetic mean. | ## Code path coverage -9 of 9 listed numerical/validation paths exercised. Source: -`src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeFeatureCentroids.cpp`. -Logical phases: (a) per-cell sweep (accumulate + range-track), (b) per-feature finalize, (c) optional periodic -adjust. +17 of 27 paths exercised. The count uses the six ordinary test cases; the hidden storage-contract fixture is listed separately in the inventory. + +Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeFeatureCentroids.cpp` (218 lines), `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeFeatureCentroidsFilter.cpp` (172 lines), and `src/simplnx/Utilities/DataArrayUtilities.cpp` (529 lines; shared FeatureIds and cell tuple-count validation). | # | Phase | Path | Test case | |---|-------|------|-----------| -| 1 | (a) Sweep | Range filter `featureId ∈ [min,max)` (parallel off → full range) | All fixtures (valid ids) | -| 2 | (a) Sweep | Per-feature min/max X/Y/Z index-range tracking | Fixtures D, E (drive the periodic condition) | -| 3 | (a) Sweep | Kahan accumulate X/Y/Z + count increment | All fixtures | -| 4 | (b) Finalize | `count>0` → centroid = sum/count | A/B/C/D/E (non-empty features) | -| 5 | (b) Finalize | `count==0` → centroid stays (0,0,0) | A (fid0), B (fid0), C (fid0) | -| 6 | (c) Periodic | `IsPeriodic` + feature spans full extent + nonzero resultant → circular mean | D/E | -| 7 | (c) Periodic | Feature does not span → unchanged; near-zero resultant → arithmetic fallback | D (feature 2), F | -| 8 | (pre) | `ValidateFeatureIdsToFeatureAttributeMatrixIndexing` failure (`maxId ≥ numFeatures`, `-5351`) | `Error - FeatureId exceeds Feature AM` | -| 9 | Bulk I/O | Feature accumulation crosses a 65,536-cell block and one-cell tail | `real HDF5 65536-block tail oracle` — exact centroid 131082, -0.5, 9 in both periodic modes | - -*The preflight AttributeMatrix-null path (`-12700`) is guarded by the `AttributeMatrixSelectionParameter` and -cannot be triggered with valid arguments — not separately tested. The OOC dual-build run is deferred: the -algorithm is a feature-indexed serial sweep with no OOC-specific variant.* +| 1 | Preflight | Create a float32 centroid array with three components per feature | `Class 1 - Analytical Centroids` | +| 2 | Preflight | Missing feature AttributeMatrix → error `-12700` | *Not directly tested. Parameter validation rejects a missing selection before this guard.* | +| 3 | Validation | FeatureId exceeds the feature tuple count → error `-5351` | `Error - FeatureId exceeds Feature AM` checks an invalid result, without asserting the error code | +| 4 | Validation | Negative FeatureId → error `-5355` | *Not directly tested. No fixture supplies a negative ID.* | +| 5 | Validation | Propagate a FeatureIds bulk-read failure | `Bulk boundary and failure propagation` injects `-91911` on the second validation read and checks that output remains zero | +| 6 | Sweep | Cancellation before a chunk → return | *Not directly tested. No fixture injects cancellation.* | +| 7 | Sweep | Read a full 65,536-cell chunk and a partial tail | `Bulk boundary and failure propagation` checks centroids across the boundary | +| 8 | Sweep | Propagate a FeatureIds read failure during accumulation | *Not directly tested. The injected second-read failure occurs during validation.* | +| 9 | Sweep | Skip a negative FeatureId | *Not directly tested. Validation rejects negative IDs before the sweep.* | +| 10 | Sweep | Accumulate cells assigned to feature 0 | *Not directly tested. Ordinary fixtures leave feature 0 empty; only the hidden storage-contract fixture checks its nonzero centroid.* | +| 11 | Sweep | Track index ranges and accumulate X, Y and Z with Kahan sums | `Class 1 - Analytical Centroids`, fixtures A–C | +| 12 | Sweep | Periodic mode accumulates unit vectors | `Class 1/4 - Periodic Boundary`, fixtures D–F | +| 13 | Finalize | Nonempty feature → divide each sum by its count | `Class 1 - Analytical Centroids`, fixtures A–C | +| 14 | Finalize | Empty feature → retain zero centroid | `Class 1 - Analytical Centroids`, fixtures A–C | +| 15 | Periodic | Periodic mode off → retain arithmetic means | `Class 1/4 - Periodic Boundary`, fixtures D and E, non-periodic controls | +| 16 | Periodic | Empty feature → skip adjustment | `Class 1/4 - Periodic Boundary`, empty feature 0 in fixtures D–F | +| 17 | Periodic | Feature does not span an axis → retain its component | `Class 1/4 - Periodic Boundary`, fixture D, feature 2 | +| 18 | Periodic | Resultant below `1e-6` → retain arithmetic mean | `Class 1/4 - Periodic Boundary`, fixture F, X component | +| 19 | Periodic | Negative angle → add a full turn before conversion | `Class 1/4 - Periodic Boundary`, fixture D, X = 3.5 | +| 20 | Periodic | Nonnegative angle → convert directly to a physical coordinate | `Class 1/4 - Periodic Boundary`, fixture E, X = 11 | +| 21 | Periodic | No component adjusted → omit adjustment message | *Not directly tested. The one-cell Y and Z extents still trigger component adjustment in fixture F.* | +| 22 | Periodic | At least one component adjusted → send adjustment message | `Class 1/4 - Periodic Boundary`, fixtures D–F exercise the path without checking the message | +| 23 | Output | Publish finalized centroids with one bulk write | `Class 1 - Analytical Centroids` and `Bulk boundary and failure propagation` | +| 24 | Output | Propagate a centroid write failure | *Not directly tested. No fixture injects an output-store failure.* | +| 25 | Sweep | Empty image → skip all chunks | *Not directly tested. Every fixture has at least one cell.* | +| 26 | Periodic | Wrap a feature across a multi-cell Y or Z extent | *Not directly tested. Periodic fixtures use one-cell Y and Z extents.* | +| 27 | Preflight | Cell Feature Ids tuple count differs from the selected geometry's cell count → error `-12701` | `Geometry cell tuple count` checks 0, 11 and 13 tuples against 12 cells | ## Test inventory +The inventory contains six ordinary test cases and one hidden storage-contract fixture. + | Test case | Status | Notes | |-----------|--------|-------| -| `SimplnxCore::ComputeFeatureCentroidsFilter` (consistency-with-self) | retired | Compared a fresh run against the sibling `Centroids` array already in `6_6_stats_test_v2.dream3d` (prior DREAM3D output) — a circular oracle. Replaced by the analytical fixtures. | -| `SimplnxCore::ComputeFeatureCentroidsFilter: Class 1 - Analytical Centroids` | new-for-V&V | Fixtures A/B/C (3 SECTIONs); hand-derived centroids at 1e-4 tolerance. | -| `SimplnxCore::ComputeFeatureCentroidsFilter: Class 1/4 - Periodic Boundary` | new-for-V&V | Fixtures D/E/F; asymmetric minimum-image means, periodic fire condition, non-unit spacing, and domain-filling fallback. | -| `SimplnxCore::ComputeFeatureCentroidsFilter: Error - FeatureId exceeds Feature AM` | new-for-V&V | Pins the `-5351` validation error path. | -| `SimplnxCore::ComputeFeatureCentroidsFilter: SIMPL Backwards Compatibility` | kept | `DYNAMIC_SECTION` over SIMPL 6.5 (UUID) + 6.4 (Filter_Name); validates UUID + 3 argument values. Conversion coverage only. | -| `Bulk boundary and failure propagation` | kept | Existing bulk-read boundary and injected-failure regression. | -| `real HDF5 65536-block tail oracle` | new-for-V&V | Reuses CentroidToy::Build and Run; checks exact physical centroids for background, the two-cell feature across the boundary, and an empty feature. | - -All active TEST_CASEs pass in-core (`NX-Com-Qt69-Vtk96-Rel`). OOC dual-build deferred (see Code path coverage). - -OOC recertification (2026-09-21): 5/5 registered CTest entries pass in each DREAM3DNX build. The new hidden case passes 35 assertions across periodic off/on. Input FeatureIds and output Centroids are asserted HDF5-OOC. The two contributing X coordinates are 131081 and 131083, so the expected mean is exactly 131082. Upstream numerical assertions and tolerances remain unchanged; legacy deviation dispositions are retained without claiming a new binary comparison. +| `SimplnxCore::ComputeFeatureCentroidsFilter: real HDF5 65536-block tail oracle` | new-for-V&V | Hidden. Defines exact centroids for feature 0, a two-cell feature across the block boundary and an empty feature, with periodic mode off and on. | +| `SimplnxCore::ComputeFeatureCentroidsFilter: Class 1 - Analytical Centroids` | new-for-V&V | Fixtures A–C check arithmetic means, empty features, a single-cell feature and 3D Z indexing at margin `1e-4`. | +| `SimplnxCore::ComputeFeatureCentroidsFilter: Class 1/4 - Periodic Boundary` | new-for-V&V | Fixtures D–F check X = 3.5 and X = 11 for wrapped features, an unchanged non-spanning feature and the domain-filling fallback. D and E replace the constant-offset expectations (`ComputeFeatureCentroidsFilter-D4`). | +| `SimplnxCore::ComputeFeatureCentroidsFilter: Error - FeatureId exceeds Feature AM` | new-for-V&V | Supplies ID 5 with two feature tuples and requires an invalid execution result. | +| `SimplnxCore::ComputeFeatureCentroidsFilter: Bulk boundary and failure propagation` | kept | Checks physical centroids across 65,536 cells plus a tail; checks validation-read error `-91911` and an unchanged zero output. | +| `SimplnxCore::ComputeFeatureCentroidsFilter: SIMPL Backwards Compatibility` | kept | Dynamic variants `SIMPL 6.5 (UUID)` and `SIMPL 6.4 (Filter_Name)` check one filter, its UUID, empty comments and three converted argument values. | +| `SimplnxCore::ComputeFeatureCentroidsFilter: Geometry cell tuple count` | new-for-V&V | For a 12-cell image, rejects 0, 11 and 13 tuples with error `-12701` and checks the input path and counts in the message. Accepts 12 tuples in another Attribute Matrix with a different tuple shape. | ## Exemplar archive -- **Archive:** None — the oracle is encoded as inline Class 1 (Analytical) + Class 4 (Invariant) fixtures in - `test/ComputeFeatureCentroidsTest.cpp`; there is no `.dream3d` gold-master to hash. -- **SHA512:** N/A (no archive). +- **Archive:** None — inputs and analytical expectations are defined in the test source. +- **SHA512:** Not applicable; these tests consume no exemplar archive from `src/Plugins/SimplnxCore/test/CMakeLists.txt`. - **Provenance:** `src/Plugins/SimplnxCore/vv/provenance/ComputeFeatureCentroidsFilter.md` -The prior circular consistency test and its `TestFileSentinel` dependency were retired. `6_6_stats_test_v2.tar.gz` -is kept in `test/CMakeLists.txt` (five other tests still consume it) but is no longer used by this filter's -tests; `6_6_find_feature_centroids.tar.gz` is kept (used by the ExtractComponentAsArray and -WriteAbaqusHexahedron tests — not an orphan). - -## Deviations from DREAM3D 6.5.171 +## Deviations from DREAM.3D v6.5.171 -Comparison by source inspection of 6.5.171/6.5.172 `FindFeatureCentroids.cpp` vs SIMPLNX, backed by the Class 1 -analytical fixtures that verify SIMPLNX independently. Full entries in -`vv/deviations/ComputeFeatureCentroidsFilter.md`: +See `vv/deviations/ComputeFeatureCentroidsFilter.md` for the source comparison, affected users and recommendations. Only Part 1 entries appear here. -- `ComputeFeatureCentroidsFilter-D1` — *precision* — SIMPLNX fetches voxel centers as `float64`, legacy as - `float32` then promotes; both store `float32`, residual ≤ 1 float32 ULP. Trust SIMPLNX. -- `ComputeFeatureCentroidsFilter-D2` — *feature addition* — `Is Periodic` is SIMPLNX-only; default `false` - reproduces legacy exactly. (Includes the two SIMPLNX-internal periodic-centroid fixes: spacing and the - constant-offset model — neither is a legacy diff because legacy has no periodic path.) -- **Retracted candidate** — the DRAFT "Kahan-vs-naive summation" precision deviation: legacy already uses the - identical Kahan kernel, so no such deviation exists (the residual precision difference is captured by D1). +| Deviation | Observed difference | +|-----------|---------------------| +| `ComputeFeatureCentroidsFilter-D1` | SIMPLNX computes cell-center coordinates in float64; DREAM.3D v6.5.171 rounds them to float32 before accumulation. | +| `ComputeFeatureCentroidsFilter-D2` | SIMPLNX adds an optional periodic centroid calculation. DREAM.3D v6.5.171 has no periodic mode. | diff --git a/src/Plugins/SimplnxCore/vv/ComputeFeatureNeighborsFilter.md b/src/Plugins/SimplnxCore/vv/ComputeFeatureNeighborsFilter.md index a6be2777ab..fcdbb31c0b 100644 --- a/src/Plugins/SimplnxCore/vv/ComputeFeatureNeighborsFilter.md +++ b/src/Plugins/SimplnxCore/vv/ComputeFeatureNeighborsFilter.md @@ -1,118 +1,148 @@ # V&V Report: ComputeFeatureNeighborsFilter -| | | -|-----------|------------------| -| Plugin | SimplnxCore | -| SIMPLNX UUID | `7177e88c-c3ab-4169-abe9-1fdaff20e598` | -| SIMPLNX Human Name | Compute Feature Neighbors | -| DREAM3D 6.5.171 equivalent | `FindNeighbors` (SIMPL UUID `97cf66f8-7a9b-5ec2-83eb-f8c4c8a17bac`) — `Source/Plugins/Statistics/StatisticsFilters/FindNeighbors.{h,cpp}` | -| Verified commit | ** | -| Status | COMPLETE | -| Sign-off | Nathan Young, 06-23-2026 | +| | | +|--------|--------------| +| Plugin | SimplnxCore | +| SIMPLNX UUID | `7177e88c-c3ab-4169-abe9-1fdaff20e598` | +| SIMPLNX Human Name | Compute Feature Neighbors | +| DREAM.3D v6.5.171 equivalent | `FindNeighbors` (SIMPL UUID `97cf66f8-7a9b-5ec2-83eb-f8c4c8a17bac`) | +| Verified commit | ** | +| Status | COMPLETE | +| Sign-off | Nathan Young, 2026-06-23; second engineer: Michael Jackson, 2026-06-30 | ## At a glance -| Aspect | Current state | -|------------------------|--------------------------| -| Algorithm Relationship | **Minor changes** of legacy `FindNeighbors::execute()`. Same core adjacency-scan algorithm; SIMPLNX adds explicit 2D/1D/single-voxel dimensionality dispatch via `NeighborUtilities`, template-specializes on the four `(StoreSurface, StoreBoundary)` combinations, and fixes two legacy bugs (D1 — SSA formula wrong for non-Z-normal faces; D2 — SurfaceFeatures incorrectly marks all features as surface for 1D and 2D EmptyY/EmptyX images). | -| Oracle (confirmed) | **Class 1 (Analytical) primary** — 37 structured TEST_CASEs with inline hand-derived expected outputs for all five output arrays across all 8 image dimensionalities and all 4 optional-output combinations. All pass. | -| Code paths enumerated | 19 of 21 listed paths exercised. The feature-range guard and cancellation remain explicitly untested. | -| Tests today | 37 registered cases plus one hidden real-HDF5 five-slice analytical case; both CTest selections pass 37/37. | -| Exemplar archive | `6_6_stats_test_v2.tar.gz` — shared SmallIn100 input used for legacy comparison only; no oracle outputs (inline expected values used for all structured tests). SSA arrays in the archive reflect the buggy 6.5.171 output and are explicitly skipped in the legacy comparison test. | -| Legacy comparison | **Run** on the SmallIn100 fixture (`6_6_stats_test_v2.tar.gz`): NumNeighbors, NeighborList, SurfaceFeatures bit-identical (3D dataset). **Plus a targeted A/B (2026-06-29)** on degenerate/anisotropic inputs through stock 6.5.171 and SIMPLNX that **proves both D1 and D2** via a surgically patched local build of the legacy source: D1 SSA `[8,8]`→`[24,24]`==NX (anisotropic), D2 SurfaceFeatures `[0,1,1]`→`[0,1,0]`==NX (EmptyY). The legacy-source patches reproduce SIMPLNX exactly; NumNeighbors/NeighborList/BoundaryCells byte-identical across all three. See deviations file. | -| Bug flags | **D1** — SharedSurfaceAreaList uses wrong area formula for non-Z-normal faces in 6.5.171. **D2** — SurfaceFeatures incorrectly marks all features as surface for 1D images and 2D EmptyY/EmptyX images in 6.5.171. | -| V&V phase | Historical COMPLETE status and sign-off retained. Section 4.3 adds a real-HDF5 oracle for Scanline slice reads and all five outputs. | +| Aspect | Current state | +|--------|---------------| +| Algorithm Relationship | Minor changes to `FindNeighbors` in DREAM.3D v6.5.171. SIMPLNX keeps face adjacency and corrects face areas and surface flags. | +| Oracle (confirmed) | Class 1 analytical expectations are encoded in 35 ordinary cases in `test/ComputeFeatureNeighborsTest.cpp`. All pass in CI. | +| Code paths enumerated | 25 of 35 exercised by the mapped tests. Other error returns, cancellation, negative IDs and the unreachable dispatch fallback have no direct fixture. | +| Tests today | 39 test cases: 35 analytical cases, one archive regression, one SIMPL conversion case, one tuple-count validation case and one hidden HDF5 case. The analytical cases cover eight dimensionality states and all four optional-output combinations. | +| Exemplar archive | `6_6_stats_test_v2.tar.gz` supplies Small IN100 inputs and three regression reference arrays. Shared surface areas use independent inline expectations. | +| Legacy comparison | Run against DREAM.3D v6.5.171 on 2026-10-22: 1 of 4 cases matches; the differences come from `ComputeFeatureNeighborsFilter-D1` and `ComputeFeatureNeighborsFilter-D2`. A local proof version with those fixes matches DREAM3D-NX in 4 of 4 cases. | +| Bug flags | None. | +| V&V phase | COMPLETE. | ## Summary -`ComputeFeatureNeighborsFilter` identifies contiguous neighboring features in an ImageGeometry by scanning face-adjacent voxel pairs with differing non-zero feature IDs, accumulating the shared surface area per feature-neighbor pair. Verification used **Class 1 (Analytical) inline oracles** across 37 hand-built test cases spanning all 8 image dimensionalities (including 1D, 2D, 3D, and single-voxel), all four optional-output combinations, and non-square 2D regression fixtures that guard a previously-fixed stride bug. Review of the legacy `FindNeighbors.cpp` source found two bugs fixed by the SIMPLNX rewrite: D1 — the SSA formula at line 431 (`float area = float(number) * xRes * yRes`) uses X×Y face area for all faces regardless of face-normal direction; D2 — the SurfaceFeatures boundary check (lines 330–343) only handles the ZPoints==1 special case, incorrectly marking every feature as surface for any image with YPoints==1 or XPoints==1. NumNeighbors, NeighborList, and BoundaryCells are bit-identical to 6.5.171 on the 3D SmallIn100 fixture. +`ComputeFeatureNeighborsFilter` finds features that share voxel faces and sums their shared face areas. Class 1 fixtures check neighbor counts, neighbor lists, areas, boundary-cell counts and surface-feature flags. All pass in CI. ## Algorithm Relationship -*Classification:* **Minor changes** ~~| Port | Rewrite | New filter~~ +*Classification:* Minor changes -*Evidence:* Same SIMPL UUID retained (`97cf66f8-7a9b-5ec2-83eb-f8c4c8a17bac`). SIMPL 6.4/6.5 conversion fixtures at `test/simpl_conversion/6_*/ComputeFeatureNeighborsFilter.json`. Core algorithm (face-adjacent voxel scan, per-feature-pair surface-area accumulation) is preserved. The SIMPLNX implementation adds template dispatch over 8 image dimensionality states and 4 optional-output combinations, which did not exist in the legacy. The SSA computation was also corrected (see D1). - -*Port-time deltas:* - -1. **Dimensionality dispatch** — legacy operated on 3D coordinates uniformly; SIMPLNX dispatches to one of 8 `ImageDimensionStateT` specializations (`SingleVoxelImage`, `ZImage1D`, `YImage1D`, `XImage1D`, `EmptyZImage2D`, `EmptyYImage2D`, `EmptyXImage2D`, `Image3D`) via `NeighborUtilities`. This also fixes the SurfaceFeatures bug for 1D and 2D EmptyY/X geometries (D2 — see below). -2. **SSA formula corrected** — legacy finalization at `FindNeighbors.cpp:431`: `float area = float(number) * xRes * yRes` uses X×Y spacing for every shared face regardless of face orientation. SIMPLNX uses `computeFaceSurfaceAreas(spacing)` (`NeighborUtilities.hpp:322`) which returns `{zFace, yFace, xFace, xFace, yFace, zFace}` = `{sx*sy, sx*sz, sy*sz, sy*sz, sx*sz, sx*sy}` and accumulates per-contact directly. This is D1. -3. **`std::map` accumulation** — accumulates `{neighborFeatureId → sharedSurfaceArea}` per feature into a sorted map; iterating the map populates NeighborList and SSA in ascending neighbor-ID order. Legacy used a two-pass approach: raw per-contact push into `vector>` followed by deduplication via `QMap` count — yielding the same NumNeighbors and NeighborList but the wrong SSA (D1). -4. **SurfaceFeatures 2D/1D fix** — legacy `FindNeighbors.cpp:330–343` only handles `ZPoints==1` explicitly; the 3D branch fires for any image with `YPoints==1` or `XPoints==1` and always triggers on `row==0` or `column==0` (both always true when that axis has length 1), marking every feature as surface. SIMPLNX uses per-dimensionality specializations that only mark corner cells (1D), corner+edge cells (2D), or all boundary cells (3D) as surface. This is D2. -5. **Two-stage boundary split** — Stage 1 processes corner/edge/face boundary cells with explicit face-validity checks; Stage 2 processes internal cells without validity overhead. Structure is new; behavior-equivalent for 3D inputs. -6. **Template specialization on optional outputs** — `ComputeFeatureNeighborsFunctor` avoids runtime branches in the per-voxel hot path. Performance only; no behavior delta. -7. **Throttled progress** — legacy used direct `notifyStatusMessage`; SIMPLNX emits throttled progress feedback. UX-only. - -*Material PRs since baseline (2025-10-01):* - -- **PR #1590** — "ENH: Standardize 2D Image Handling" (2026-03-xx) — extracted `NeighborUtilities` as a shared module; added explicit 2D/1D dimensionality dispatch to this filter. Non-square 2D regression tests added during V&V catch the stride bug, since resolved. +*Evidence:* `FindNeighbors::execute()` and the current Direct and Scanline implementations use distinct positive face-neighbor IDs; SIMPLNX adds dimensionality dispatch and the two corrections below. ## Oracle -*Class:* **1 (Analytical)** primary. +*Class:* 1 (Analytical). -*Applied:* Expected outputs for all five arrays (NumNeighbors, NeighborList, SharedSurfaceAreaList, BoundaryCells, SurfaceFeatures) are derived by hand from the input FeatureIds array and the geometry's dimensions and spacing. For each geometry fixture, the engineer traced every face-adjacent voxel pair, identified pairs with differing non-zero feature IDs, accumulated the per-pair face area (= product of the two spacings perpendicular to the face normal), and summed shared areas per feature-neighbor pair. BoundaryCells counts = the number of distinct-feature face neighbors for each voxel. SurfaceFeatures = true for features that have any voxel on the image boundary. The derivations are embedded as inline `std::array` literals directly adjacent to the `REQUIRE`-equivalent comparisons in the test source. +*Applied:* Hand-specified FeatureIds define adjacency, face counts, direction-specific face areas and image-boundary contact in each active dimension. -*Encoded:* `test/ComputeFeatureNeighborsTest.cpp` — 37 TEST_CASEs. The `ExecuteFilter()` helper executes the filter and compares all requested outputs against the hand-derived inline exemplar arrays via `UnitTest::CompareArrays` and `UnitTest::CompareNeighborLists`. All 37 pass at the verified commit. +*Encoded:* `src/Plugins/SimplnxCore/test/ComputeFeatureNeighborsTest.cpp::SimplnxCore::ComputeFeatureNeighborsFilter: Case 0.0.0: Single Voxel - Full Execution` and the other `Case` tests encode 11 geometry fixtures in 35 cases. All pass in CI. -*Second-engineer review:* **Signed off by Nathan Young, 2026-06-23.** Review focus: the 3D 5×5×5 fixture (125-voxel, 6-feature, 7-neighbor-pair) and the non-square 2D stride regression fixtures as the highest-complexity cases. +*Second-engineer review:* Michael Jackson, 2026-06-30. ## Bugs found and fixed -Existing D1 and D2 corrections remain in the branch and are described in the deviation sidecar. This OOC recertification changes no production algorithm and found no new defect. +All defects in this table are fixed. DREAM3D-NX v7.4.2 contains the fixes for `ComputeFeatureNeighborsFilter-D1` and `ComputeFeatureNeighborsFilter-D2`. + +| Deviation | Defect | Affected released versions | Resolution | +|-----------|--------|----------------------------|------------| +| `ComputeFeatureNeighborsFilter-D1` | Shared areas used X spacing times Y spacing for every face direction. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | Each contact contributes the product of the two spacings perpendicular to its face normal. | +| `ComputeFeatureNeighborsFilter-D2` | A unit X or Y dimension caused interior features to be marked as surface features. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | Only boundary contact in active dimensions sets the flag; a single voxel is a surface feature. | ## Code path coverage -19 of 21 listed paths exercised. - -Source: `Algorithms/ComputeFeatureNeighbors.cpp` (26 lines), `ComputeFeatureNeighborsDirect.cpp` (394 lines), and `ComputeFeatureNeighborsScanline.cpp` (250 lines). - -The algorithm has four logical stages: **(a) Guard** — validates maxFeatureId and sets up geometry; **(b) Dispatch** — selects dimensionality template and optional-output template; **(c) Stage 1** — processes boundary voxels (corners, then edges, then face-interior boundary cells); **(d) Stage 2** — processes interior voxels (3D only); **(e) Finalize** — builds NumNeighbors + NeighborList + SharedSurfaceAreaList from the per-feature map. - -| # | Stage | Path | Test case | -|----|-------------|---------------------|--------------------------| -| 1 | (a) Guard | `maxFeatureId >= totalFeatures` → error result | *Not directly tested. Low-value guard; exercised implicitly when FeatureIds and FeatureAM are mismatched at the filter level.* | -| 2 | (b) Dispatch | `StoreSurface=true, StoreBoundary=true` | `Case *.*.0: * - Full Execution` (all dimensionalities) | -| 3 | (b) Dispatch | `StoreSurface=true, StoreBoundary=false` | `Case *.*.1: * - No Boundary` (all dimensionalities) | -| 4 | (b) Dispatch | `StoreSurface=false, StoreBoundary=true` | `Case *.*.2: * - No Surface Features` (all dimensionalities) | -| 5 | (b) Dispatch | `StoreSurface=false, StoreBoundary=false` | `Case *.*.3: * - No Optionals` (all dimensionalities) | -| 6 | (b) Dispatch | Dimensionality → `SingleVoxelImage` | `Case 0.0.*: Single Voxel` | -| 7 | (b) Dispatch | Dimensionality → `ZImage1D` / `YImage1D` / `XImage1D` | `Case 1.0.*` / `Case 1.1.*` / `Case 1.2.*` | -| 8 | (b) Dispatch | Dimensionality → `EmptyZImage2D` / `EmptyYImage2D` / `EmptyXImage2D` (square) | `Case 2.0.*` / `Case 2.1.*` / `Case 2.2.*` | -| 9 | (b) Dispatch | Dimensionality → `EmptyZImage2D` / `EmptyYImage2D` / `EmptyXImage2D` (non-square stride regression) | `Case 2.0.4` / `Case 2.1.4` / `Case 2.2.4` | -| 10 | (b) Dispatch | Dimensionality → `Image3D` | `Case 3.0.*: 3D` | -| 11 | (c) Stage 1 | `featureId == 0` → skip voxel (no surface/boundary contribution) | 1D test (`featureIdsArray[5] = 0`) + 3D test (multiple zero voxels) | -| 12 | (c)/(d) | `neighborFeatureId == 0` → skip face (background neighbor) | 1D test + 3D test (background voxels present) | -| 13 | (c) Stage 1 | `!isValidFaceNeighbor[faceIndex]` → skip face (image boundary) | All tests — every fixture has at least one image-boundary voxel | -| 14 | (c)/(d) | `neighborFeatureId == feature` → skip face (same-feature neighbor) | All multi-voxel tests (interior voxels within a feature) | -| 15 | (c)/(d) | Normal accumulation — different non-zero features → `neighborSurfaceAreas[feature][nbr] += area` | All multi-feature tests | -| 16 | (c) Stage 1 | `ProcessSurfaceFeaturesV=true` → `surfaceFeatures->setValue(feature, true)` for boundary voxel | `Case *.*.0` and `Case *.*.1` (Full Execution + No Boundary variants) | -| 17 | (c)/(d) | `ProcessBoundaryCellsV=true` → `boundaryCells->setValue(voxelIndex, numDiffNeighbors)` | `Case *.*.0` and `Case *.*.2` (Full Execution + No Surface Features variants) | -| 18 | (c) Stage 1 | Edge cell processing path (ProcessEdges — skipped for SingleVoxelImage) | `Case 1.0.*` through `Case 3.0.*` | -| 19 | (c) Stage 1 | Face cell processing path (ProcessFaces — 2D and 3D only, no validity check needed) | `Case 2.*.*` and `Case 3.0.*` | -| 20 | (d) Stage 2 | `shouldCancel` → early return from 3D internal loop | *Not directly tested. Requires cancel-signal injection infrastructure not present in this test suite.* | -| 21 | Scanline | Contacts span five rolling 25-cell slices with HDF5 inputs and outputs | `real HDF5 five-slice analytical oracle` — original hand-derived counts, adjacency, areas, boundary flags, and surface flags | +25 of 35 paths exercised. Each uncovered path states why it has no direct fixture. + +Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeFeatureNeighbors.cpp` (26 lines), `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeFeatureNeighborsDirect.cpp` (394 lines), `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeFeatureNeighborsScanline.cpp` (250 lines), `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeFeatureNeighborsFilter.cpp` (230 lines), and `src/simplnx/Utilities/NeighborUtilities.hpp` (634 lines). + +Test names below omit the common `SimplnxCore::ComputeFeatureNeighborsFilter:` prefix. Direct separates boundary and interior cells; Scanline uses three rolling slices. + +| # | Phase | Path | Test case | +|---|-------|------|-----------| +| 1 | Validation | Maximum FeatureId exceeds the feature array bounds; return `-24500` | *Not directly tested. No fixture supplies an out-of-range positive ID.* | +| 2 | Optional outputs | Store both surface flags and boundary counts | `Case 0.0.0` through `Case 3.0.0` — Full Execution variants | +| 3 | Optional outputs | Store surface flags only | `Case 0.0.1` through `Case 3.0.1` — No Boundary variants | +| 4 | Optional outputs | Store boundary counts only | `Case 0.0.2` through `Case 3.0.2` — No Surface Features variants | +| 5 | Optional outputs | Store neither optional output | `Case 0.0.3` through `Case 3.0.3` — No Optionals variants | +| 6 | Dimensions | Single voxel | `Case 0.0.0` through `Case 0.0.3` | +| 7 | Dimensions | 1D along Z, Y or X | `Case 1.0.0`, `Case 1.1.0`, `Case 1.2.0` | +| 8 | Dimensions | Square 2D with unit Z, Y or X | `Case 2.0.0`, `Case 2.1.0`, `Case 2.2.0` | +| 9 | Dimensions | Non-square 2D with unit Z, Y or X | `Case 2.0.4`, `Case 2.1.4`, `Case 2.2.4` | +| 10 | Dimensions | Full 3D image | `Case 3.0.0` through `Case 3.0.3` | +| 11 | Cell scan | FeatureId zero contributes no contacts or surface flag | `Case 1.0.0`, `Case 3.0.0` | +| 12 | Neighbor scan | Neighbor ID zero contributes no face | `Case 1.0.0`, `Case 3.0.0` | +| 13 | Neighbor scan | Face outside the image is skipped | `Case 0.0.0`, `Case 3.0.0` | +| 14 | Neighbor scan | Same-feature face is skipped | `Case 1.0.0`, `Case 3.0.0` | +| 15 | Accumulation | Distinct positive neighbor adds its direction-specific face area | `Case 2.0.0`, `Case 2.1.0`, `Case 2.2.0`, `Case 3.0.0` | +| 16 | Surface flags | Mark boundary features and leave interior features false | `Case 1.0.0`, `Case 2.1.0`, `Case 2.2.0`, `Case 3.0.0` | +| 17 | Boundary counts | Write the number of distinct-feature face contacts for each cell | `Case 1.0.0`, `Case 3.0.0` | +| 18 | Direct boundary | Process edge cells; omit them for a single voxel | `Case 0.0.0`, `Case 1.0.0`, `Case 2.0.0`, `Case 3.0.0` | +| 19 | Direct boundary | Process face interiors in 2D and 3D | `Case 2.0.0`, `Case 3.0.0` | +| 20 | Cancellation | Return during the Direct interior sweep or Scanline slice loop | *Not directly tested. No fixture injects cancellation.* | +| 21 | Scanline | Read first and second slices, rotate buffers, then read later slices | `Case 3.0.0` — in-memory algorithm scenarios | +| 22 | Direct validation | Missing surface store returns `-789620` | *Not directly tested. Normal filter execution creates the requested output.* | +| 23 | Direct validation | Missing boundary store returns `-789621` | *Not directly tested. Normal filter execution creates the requested output.* | +| 24 | Scanline I/O | First input-slice read fails | *Not directly tested. Requires an injected store read failure.* | +| 25 | Scanline I/O | Second input-slice read fails | *Not directly tested. Requires an injected store read failure.* | +| 26 | Scanline I/O | Later input-slice read fails after buffer rotation | *Not directly tested. Requires an injected store read failure.* | +| 27 | Scanline I/O | Boundary-slice write fails | *Not directly tested. Requires an injected store write failure.* | +| 28 | Cell scan | Negative feature or neighbor ID contributes no contact | *Not directly tested. Fixtures use nonnegative IDs.* | +| 29 | Finalize | Write sorted neighbor IDs, counts and areas; isolated features have empty lists | `Case 1.0.0`, `Case 3.0.0` | +| 30 | Dispatch | Select Direct and assert output values | `Case 0.0.0` through `Case 3.0.3` — in-memory algorithm scenarios | +| 31 | Dispatch | Select Scanline and assert output values | `Case 0.0.0` through `Case 3.0.3` — in-memory algorithm scenarios | +| 32 | Preflight | Create required outputs and selected optional outputs | `Case 0.0.0` through `Case 0.0.3` — preflight succeeds and execution checks requested outputs | +| 33 | Direct interior | Accumulate all six faces without image-boundary guards | `Case 3.0.0` | +| 34 | Direct dispatch | Return after no dimensionality specialization matches | *Not directly tested. The eight dimensionality states exhaust all unit-axis combinations.* | +| 35 | Preflight | FeatureIds tuple count differs from the selected geometry's cell count; return `-24501` | `Geometry cell tuple count` — 0, 11 and 13 tuples for a 12-cell geometry | ## Test inventory -| Test case | Status | Notes | -|------------------|-------------|-----------------------------| -| `Case 0.0.0` through `Case 0.0.3`: Single Voxel (4 cases) | kept | 1-voxel geometry, 1 feature; verifies zero-neighbor result and optional-output combinations | -| `Case 1.0.0` through `Case 1.0.3`: 1D Z (4 cases) | kept | 7-cell 1D Z strip, 4 features + background; verifies 1D neighbor detection and SSA = face area | -| `Case 1.1.0` through `Case 1.1.3`: 1D Y (4 cases) | kept | Same layout rotated to Y axis; same expected values with Y spacing applied | -| `Case 1.2.0` through `Case 1.2.3`: 1D X (4 cases) | kept | Same layout rotated to X axis; same expected values with X spacing applied | -| `Case 2.0.0` through `Case 2.0.3`: 2D Empty Z, square 5×5×1 (4 cases) | kept | 5×5×1 geometry, 5 features + background; non-trivial 2D neighbor graph with anisotropic spacing | -| `Case 2.1.0` through `Case 2.1.3`: 2D Empty Y, square 5×1×5 (4 cases) | kept | Same layout rotated to Z-X plane | -| `Case 2.2.0` through `Case 2.2.3`: 2D Empty X, square 1×5×5 (4 cases) | kept | Same layout rotated to Y-Z plane | -| `Case 2.0.4`: 2D Empty Z, non-square 3×2×1| new-for-V&V | Stride regression: dims[0] ≠ dims[1]; wrong stride produces SSA = 2×area instead of 3×area | -| `Case 2.1.4`: 2D Empty Y, non-square 3×1×2| new-for-V&V | Same stride regression rotated to Z-X plane | -| `Case 2.2.4`: 2D Empty X, non-square 1×3×2| new-for-V&V | Same stride regression rotated to Y-Z plane | -| `Case 3.0.0` through `Case 3.0.3`: 3D 5×5×5 (4 cases) | kept | 125-voxel, 6-feature, fully 3D geometry with anisotropic spacing; 7 feature-pair SSA values hand-derived | -| `Legacy: SmallIn100` | kept | Legacy comparison on `6_6_stats_test_v2.tar.gz`; compares NumNeighbors, NeighborList, SurfaceFeatures; SSA skipped (D1) | -| `SIMPL Backwards Compatibility` (2 DYNAMIC_SECTIONs: "SIMPL 6.5 (UUID)", "SIMPL 6.4 (Filter_Name)") | kept | Validates UUID + argument-key + parameter-value decoding from SIMPL JSON; does not execute the filter | -| `real HDF5 five-slice analytical oracle` | new-for-V&V | Reuses Create3DDataStructure and ExecuteFilter; all five analytical outputs match while target-only counters prove OOC-on-OOC dispatch. | - -On 2026-09-21, the registered selections pass 37/37 in both DREAM3DNX builds. The hidden HDF5 case passes 73 assertions. FeatureIds and the three fixed-size output arrays are asserted HDF5-OOC; both output NeighborLists are asserted out-of-core. The existing 125-cell fixture spans five algorithm slices, so a missing Z neighbor or incorrect rolling buffer changes independently specified outputs. Existing upstream assertions and tolerances are preserved. Historical D1/D2 dispositions remain valid; no new legacy binary run is claimed. +The 38 ordinary test cases cover analytical expectations, archive regression, conversion and tuple-count validation. The 35 analytical cases, archive regression and conversion case pass in CI. + +| Test case | Status | Notes | +|-----------|--------|-------| +| `SimplnxCore::ComputeFeatureNeighborsFilter: real HDF5 five-slice analytical oracle` | new-for-V&V | Hidden. Reuses the 5×5×5 analytical fixture; checks all five outputs, HDF5 stores and the selected algorithm counter. | +| `SimplnxCore::ComputeFeatureNeighborsFilter: Case 0.0.0: Single Voxel - Full Execution` | kept | One voxel: zero neighbors, empty lists and a surface feature. Checks all five outputs. | +| `SimplnxCore::ComputeFeatureNeighborsFilter: Case 0.0.1: Single Voxel - No Boundary` | kept | One voxel: zero neighbors, empty lists and a surface feature. Checks counts, lists, areas and surface flags. | +| `SimplnxCore::ComputeFeatureNeighborsFilter: Case 0.0.2: Single Voxel - No Surface Features` | kept | One voxel: zero neighbors, empty lists and a surface feature. Checks counts, lists, areas and boundary counts. | +| `SimplnxCore::ComputeFeatureNeighborsFilter: Case 0.0.3: Single Voxel - No Optionals` | kept | One voxel: zero neighbors, empty lists and a surface feature. Checks counts, lists and areas. | +| `SimplnxCore::ComputeFeatureNeighborsFilter: Case 1.0.0: 1D Z - Full Execution` | kept | Seven cells along Z: two neighbor pairs and one isolated feature. Checks all five outputs. | +| `SimplnxCore::ComputeFeatureNeighborsFilter: Case 1.0.1: 1D Z - No Boundary` | kept | Seven cells along Z: two neighbor pairs and one isolated feature. Checks counts, lists, areas and surface flags. | +| `SimplnxCore::ComputeFeatureNeighborsFilter: Case 1.0.2: 1D Z - No Surface Features` | kept | Seven cells along Z: two neighbor pairs and one isolated feature. Checks counts, lists, areas and boundary counts. | +| `SimplnxCore::ComputeFeatureNeighborsFilter: Case 1.0.3: 1D Z - No Optionals` | kept | Seven cells along Z: two neighbor pairs and one isolated feature. Checks counts, lists and areas. | +| `SimplnxCore::ComputeFeatureNeighborsFilter: Case 1.1.0: 1D Y - Full Execution` | kept | Seven cells along Y: two neighbor pairs and one isolated feature. Checks all five outputs. | +| `SimplnxCore::ComputeFeatureNeighborsFilter: Case 1.1.1: 1D Y - No Boundary` | kept | Seven cells along Y: two neighbor pairs and one isolated feature. Checks counts, lists, areas and surface flags. | +| `SimplnxCore::ComputeFeatureNeighborsFilter: Case 1.1.2: 1D Y - No Surface Features` | kept | Seven cells along Y: two neighbor pairs and one isolated feature. Checks counts, lists, areas and boundary counts. | +| `SimplnxCore::ComputeFeatureNeighborsFilter: Case 1.1.3: 1D Y - No Optionals` | kept | Seven cells along Y: two neighbor pairs and one isolated feature. Checks counts, lists and areas. | +| `SimplnxCore::ComputeFeatureNeighborsFilter: Case 1.2.0: 1D X - Full Execution` | kept | Seven cells along X: two neighbor pairs and one isolated feature. Checks all five outputs. | +| `SimplnxCore::ComputeFeatureNeighborsFilter: Case 1.2.1: 1D X - No Boundary` | kept | Seven cells along X: two neighbor pairs and one isolated feature. Checks counts, lists, areas and surface flags. | +| `SimplnxCore::ComputeFeatureNeighborsFilter: Case 1.2.2: 1D X - No Surface Features` | kept | Seven cells along X: two neighbor pairs and one isolated feature. Checks counts, lists, areas and boundary counts. | +| `SimplnxCore::ComputeFeatureNeighborsFilter: Case 1.2.3: 1D X - No Optionals` | kept | Seven cells along X: two neighbor pairs and one isolated feature. Checks counts, lists and areas. | +| `SimplnxCore::ComputeFeatureNeighborsFilter: Case 2.0.0: 2D Empty Z - Full Execution` | kept | 5×5×1 cells: areas 22.6, 6.8 and 23.8; two interior features. Checks all five outputs. | +| `SimplnxCore::ComputeFeatureNeighborsFilter: Case 2.0.1: 2D Empty Z - No Boundary` | kept | 5×5×1 cells: areas 22.6, 6.8 and 23.8; two interior features. Checks counts, lists, areas and surface flags. | +| `SimplnxCore::ComputeFeatureNeighborsFilter: Case 2.0.2: 2D Empty Z - No Surface Features` | kept | 5×5×1 cells: areas 22.6, 6.8 and 23.8; two interior features. Checks counts, lists, areas and boundary counts. | +| `SimplnxCore::ComputeFeatureNeighborsFilter: Case 2.0.3: 2D Empty Z - No Optionals` | kept | 5×5×1 cells: areas 22.6, 6.8 and 23.8; two interior features. Checks counts, lists and areas. | +| `SimplnxCore::ComputeFeatureNeighborsFilter: Case 2.1.0: 2D Empty Y - Full Execution` | kept | 5×1×5 cells: areas 22.6, 6.8 and 23.8; two interior features. Checks all five outputs. | +| `SimplnxCore::ComputeFeatureNeighborsFilter: Case 2.1.1: 2D Empty Y - No Boundary` | kept | 5×1×5 cells: areas 22.6, 6.8 and 23.8; two interior features. Checks counts, lists, areas and surface flags. | +| `SimplnxCore::ComputeFeatureNeighborsFilter: Case 2.1.2: 2D Empty Y - No Surface Features` | kept | 5×1×5 cells: areas 22.6, 6.8 and 23.8; two interior features. Checks counts, lists, areas and boundary counts. | +| `SimplnxCore::ComputeFeatureNeighborsFilter: Case 2.1.3: 2D Empty Y - No Optionals` | kept | 5×1×5 cells: areas 22.6, 6.8 and 23.8; two interior features. Checks counts, lists and areas. | +| `SimplnxCore::ComputeFeatureNeighborsFilter: Case 2.2.0: 2D Empty X - Full Execution` | kept | 1×5×5 cells: areas 22.6, 6.8 and 23.8; two interior features. Checks all five outputs. | +| `SimplnxCore::ComputeFeatureNeighborsFilter: Case 2.2.1: 2D Empty X - No Boundary` | kept | 1×5×5 cells: areas 22.6, 6.8 and 23.8; two interior features. Checks counts, lists, areas and surface flags. | +| `SimplnxCore::ComputeFeatureNeighborsFilter: Case 2.2.2: 2D Empty X - No Surface Features` | kept | 1×5×5 cells: areas 22.6, 6.8 and 23.8; two interior features. Checks counts, lists, areas and boundary counts. | +| `SimplnxCore::ComputeFeatureNeighborsFilter: Case 2.2.3: 2D Empty X - No Optionals` | kept | 1×5×5 cells: areas 22.6, 6.8 and 23.8; two interior features. Checks counts, lists and areas. | +| `SimplnxCore::ComputeFeatureNeighborsFilter: Case 3.0.0: 3D - Full Execution` | kept | 125 cells: ten feature-pair areas, an isolated feature and an interior feature. Checks all five outputs. | +| `SimplnxCore::ComputeFeatureNeighborsFilter: Case 3.0.1: 3D - No Boundary` | kept | 125 cells: ten feature-pair areas, an isolated feature and an interior feature. Checks counts, lists, areas and surface flags. | +| `SimplnxCore::ComputeFeatureNeighborsFilter: Case 3.0.2: 3D - No Surface Features` | kept | 125 cells: ten feature-pair areas, an isolated feature and an interior feature. Checks counts, lists, areas and boundary counts. | +| `SimplnxCore::ComputeFeatureNeighborsFilter: Case 3.0.3: 3D - No Optionals` | kept | 125 cells: ten feature-pair areas, an isolated feature and an interior feature. Checks counts, lists and areas. | +| `SimplnxCore::ComputeFeatureNeighborsFilter: Case 2.0.4: 2D Empty Z - Non-Square {3,2,1}` | new-for-V&V | Six cells in two feature rows: checks one neighbor per feature, shared area 6, all boundary counts 1 and both surface flags true. | +| `SimplnxCore::ComputeFeatureNeighborsFilter: Case 2.1.4: 2D Empty Y - Non-Square {3,1,2}` | new-for-V&V | Six cells in two feature rows: checks one neighbor per feature, shared area 6, all boundary counts 1 and both surface flags true. | +| `SimplnxCore::ComputeFeatureNeighborsFilter: Case 2.2.4: 2D Empty X - Non-Square {1,3,2}` | new-for-V&V | Six cells in two feature rows: checks one neighbor per feature, shared area 6, all boundary counts 1 and both surface flags true. | +| `SimplnxCore::ComputeFeatureNeighborsFilter: Legacy: SmallIn100` | kept | Compares SurfaceFeatures, NumNeighbors and NeighborList with the Small IN100 archive. SharedSurfaceAreaList and BoundaryCells are not compared. | +| `SimplnxCore::ComputeFeatureNeighborsFilter: SIMPL Backwards Compatibility` | kept | Dynamic sections `SIMPL 6.5 (UUID)` and `SIMPL 6.4 (Filter_Name)` check identity, comments and ten converted arguments. | +| `SimplnxCore::ComputeFeatureNeighborsFilter: Geometry cell tuple count` | new-for-V&V | Checks error `-24501` for 0, 11 and 13 FeatureIds tuples against 12 geometry cells, including the input path and counts in the message. Accepts 12 tuples in another Attribute Matrix with a different tuple shape. | ## Exemplar archive @@ -120,9 +150,11 @@ On 2026-09-21, the registered selections pass 37/37 in both DREAM3DNX builds. Th - **SHA512:** `e84999dec914d81efce4fc4237c49c9bf32e48381b1e79f58aa4df934f0d7606cd7a948f9a5e7b17a126a7944cc531b531cfdc70756ca3e2207b20734e089723` - **Provenance:** `src/Plugins/SimplnxCore/vv/provenance/ComputeFeatureNeighborsFilter.md` -The archive is a shared SmallIn100 dataset used by multiple statistics filters. For `ComputeFeatureNeighborsFilter` it is consumed only by the `Legacy: SmallIn100` test as a legacy-comparison input+output fixture. Oracle outputs for all structured tests (Cases 0–3) are inline hand-derived values; no archive is needed. The archive's SSA arrays reflect the buggy 6.5.171 output and are not compared (see D1). +## Deviations from DREAM.3D v6.5.171 -## Deviations from DREAM3D 6.5.171 +See `vv/deviations/ComputeFeatureNeighborsFilter.md` for the root cause, affected users and recommendation for each Part 1 entry. -- `ComputeFeatureNeighborsFilter-D1` — SharedSurfaceAreaList uses `count * xRes * yRes` for all faces in legacy; correct formula is `Σ area(face_direction)` — see `vv/deviations/ComputeFeatureNeighborsFilter.md` -- `ComputeFeatureNeighborsFilter-D2` — SurfaceFeatures incorrectly marks all features as surface for any image with `XPoints==1` or `YPoints==1` in legacy — see `vv/deviations/ComputeFeatureNeighborsFilter.md` +| Deviation | Observed difference | +|-----------|---------------------| +| `ComputeFeatureNeighborsFilter-D1` | Shared areas use each face direction's area instead of X spacing times Y spacing for all contacts. | +| `ComputeFeatureNeighborsFilter-D2` | Surface flags use only active dimensions; interior features in 1D and unit-X or unit-Y 2D images remain false. | diff --git a/src/Plugins/SimplnxCore/vv/ComputeFeaturePhasesFilter.md b/src/Plugins/SimplnxCore/vv/ComputeFeaturePhasesFilter.md index 960f9bb305..e627668dc3 100644 --- a/src/Plugins/SimplnxCore/vv/ComputeFeaturePhasesFilter.md +++ b/src/Plugins/SimplnxCore/vv/ComputeFeaturePhasesFilter.md @@ -1,123 +1,121 @@ # V&V Report: ComputeFeaturePhasesFilter -| | | -|-----------|------------------| -| Plugin | SimplnxCore | +| | | +|--------|--------------| +| Plugin | SimplnxCore | | SIMPLNX UUID | `da5bb20e-4a8e-49d9-9434-fbab7bc434fc` | -| DREAM3D 6.5.171 equivalent | `FindFeaturePhases` (SIMPL UUID `6334ce16-cea5-5643-83b5-9573805873fa`) | -| Verified commit | *pending* | +| SIMPLNX Human Name | Compute Feature Phases | +| DREAM.3D v6.5.171 equivalent | `FindFeaturePhases` (Generic plugin, SIMPL UUID `6334ce16-cea5-5643-83b5-9573805873fa`) | +| Verified commit | ** | | Status | COMPLETE | -| Sign-off | Nathan Young - 7/15/2026 | +| Sign-off | Nathan Young, 2026-07-15; second engineer: Michael Jackson, 2026-08-06 | ## At a glance -| Aspect | Current state | -|------------------------|--------------------------| -| Algorithm Relationship | **Port with post-port hardening** — core algorithm identical; D1, D2, D3 hardening active. See §Algorithm Relationship. | -| Oracle | **Class 1 + 4 (Analytical + Invariant)** — encoded. 10 inline TEST_CASEs cover all paths except cancel-signal injection. | -| Tests | 10 inline TEST_CASEs (Cases 1–10). `SIMPL Backwards Compatibility` retained. Old `(Valid Parameters)` circular-oracle test **retired**. | -| Legacy comparison | **Complete** (2026-07-10, 3 cases). `featurePhases[1..N]` bit-identical. D1 confirmed by Case C. See `vv/deviations/`. | -| Open items | (Non-blocking) Second Engineer Review | +| Aspect | Current state | +|--------|---------------| +| Algorithm Relationship | Minor changes to DREAM.3D v6.5.171 `FindFeaturePhases`. SIMPLNX preserves cell order, skips background cells, rejects negative phases and limits warning text. | +| Oracle (confirmed) | Class 1 analytical outputs and Class 4 shape and warning checks are encoded in 10 inline fixtures. All pass in CI. | +| Code paths enumerated | 16 of 24 paths exercised. Gaps include cancellation, empty input, full chunks, I/O failures and plural warning text. | +| Tests today | 11 test cases: 10 inline fixtures and 1 SIMPL conversion test with two variants. All pass in CI. | +| Exemplar archive | `6_6_stats_test_v2.tar.gz` is retired for this filter. Current expectations are in the test source. | +| Legacy comparison | Not run on the current algorithm. No saved results support the reported 2026-07-10 comparison, and the algorithm changed afterward. | +| Bug flags | None | +| V&V phase | Analytical verification and release-source checks are complete. Second-engineer review is not recorded. | ## Summary -`ComputeFeaturePhasesFilter` assigns each Feature an Ensemble phase by reading the Cell Phase of any Element belonging to that Feature, assuming all Cells of a Feature share one phase; a consolidated warning is emitted when they do not. - -**Headline result:** `featurePhases[1..N]` is bit-identical to DREAM3D 6.5.171 `FindFeaturePhases` on Cases A (consistent phases), B (inconsistent phases, warning), and C (background cells). Post-port hardening introduced three deviations D1, D2, and D3; none affect `featurePhases[1..N]` for valid inputs. D1 confirmed empirically by Case C: SIMPL writes `featurePhases[0]=2`; SIMPLNX leaves it `0`. - -The pre-V&V `(Valid Parameters)` test was a circular oracle (compared filter output against `Cell Feature Data/Phases` in `6_6_stats_test_v2.dream3d`, which was generated by this filter's SIMPL predecessor). It has been retired and replaced by 10 inline Class 1 analytical tests. +`ComputeFeaturePhasesFilter` assigns each positive feature the phase of its last cell in input order. Hand-derived fixtures check output values, background handling, warnings and invalid inputs. All pass in CI; DREAM3D-NX v7.4.2 contains the negative-phase validation fix. ## Algorithm Relationship -*Classification:* **Port with post-port hardening** +*Classification:* Minor changes -Core algorithm body (`featurePhases[featureIds[i]] = cellPhases[i]`) is structurally identical to `FindFeaturePhases::execute()`. Same SIMPL UUID retained. All differences are additions that do not change output for valid inputs. +*Evidence:* `src/Plugins/SimplnxCore/src/SimplnxCore/SimplnxCoreLegacyUUIDMapping.hpp` maps `FindFeaturePhases` to this filter; both use the last cell phase, while SIMPLNX adds validation, background exclusion and chunked reads. -**Port-time additions:** -1. Cancel check (`m_ShouldCancel`) at top of per-cell loop (PR #1582) -2. `ValidateFeatureIdsToFeatureAttributeMatrixIndexing` guard before the loop (PR #1308) -3. Consolidated warning listing all affected feature IDs (PR #1455) +## Oracle -**Post-Phase-9 hardening (2026-07-10):** -4. Background feature skip — `if(featureId == 0) { continue; }` (D1) -5. Negative cell phase guard — error -61861 (D2) -6. Warning truncation to 15 IDs + `"and N more occurrence(s)"` suffix (D3) +*Class:* 1 (Analytical), with 4 (Invariant) shape and warning checks. -**Material PRs since baseline (2025-10-01):** #1582, #1588, #1544, #1535, #1455, #1308 +*Applied:* Hand-written cell IDs and phases determine the last phase for each positive feature; background and unused feature entries stay zero. -## Oracle +*Encoded:* `src/Plugins/SimplnxCore/test/ComputeFeaturePhasesFilterTest.cpp::SimplnxCore::ComputeFeaturePhasesFilter: Valid: Uniform Phases` and nine companion fixtures check exact outputs, warning IDs and error codes. All pass in CI. -*Class:* **1 (Analytical) primary + 4 (Invariant) companion** +*Second-engineer review:* Michael Jackson, 2026-08-06. -The algorithm is a simple indirection: `featurePhases[featureIds[i]] = cellPhases[i]`. For any toy dataset, `featurePhases[f]` equals the phase of the last cell belonging to feature `f` in index order. For consistent datasets the result is unambiguous. +## Bugs found and fixed -**Class 4 invariants:** -- `featurePhases[f] ∈ {phase values of cells with featureId == f}` for all `f > 0` -- `featurePhases` has exactly `numFeatures` tuples (matches Feature AM shape) -- Warning is emitted if and only if any `f > 0` has cells with differing phase values +All defects in this table are fixed. DREAM3D-NX v7.4.2 contains the fix for `ComputeFeaturePhasesFilter-D2`. -**Encoded:** 10 inline TEST_CASEs in `test/ComputeFeaturePhasesFilterTest.cpp`. See `vv/provenance/` for fixture derivations. +| Deviation | Defect | Affected released versions | Resolution | +|-----------|--------|----------------------------|------------| +| `ComputeFeaturePhasesFilter-D2` | A negative cell phase could be copied into feature output without an error. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | The filter rejects a negative phase on a positive feature with error `-61861`. | ## Code path coverage -Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeFeaturePhases.cpp` - -| # | Path | Test fixture | -|---|---|---| -| 1a | Validator: featureId OOB → error -5351 | Case 5 | -| 1b | Validator: negative featureId → error -5355 | Case 8 | -| 2 | Size mismatch → preflight error -61860 — check in `preflightImpl()` | Case 6 | -| 3 | `m_ShouldCancel` → early return | **Not tested** (intentional) | -| 4 | `featureId == 0` → `continue` (D1) | Cases 2, 10 | -| 5 | `currentPhaseId < 0` → error -61861 (D2) | Case 7 | -| 5b | `currentPhaseId < 0` on a background cell → skipped before the guard, no error | Case 10 | -| 6 | First occurrence for feature: store phase | Cases 1, 2 | -| 7 | Subsequent occurrence, phase matches: no conflict | Case 1 | -| 8 | Subsequent occurrence, phase differs: `warnFeature[f] = true` | Cases 3, 4 | -| 9a | `count != 0` → build and push warning | Case 3 | -| 9b | `count > 15` → append truncation suffix | Case 4 | -| 10 | `count == 0` → return result with no warnings | Cases 1, 2 | -| 11 | Feature index never written by the loop stays zero-initialized | Case 9 | +16 of 24 paths exercised. Each uncovered path is listed below. + +Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeFeaturePhases.cpp` (145 lines), `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeFeaturePhasesFilter.cpp` (148 lines), and `src/simplnx/Utilities/DataArrayUtilities.cpp` (515 lines, shared FeatureId validator). + +Test names below omit the common `SimplnxCore::ComputeFeaturePhasesFilter: ` prefix. + +| # | Phase | Path | Test case | +|---|-------|------|-----------| +| 1 | Preflight | Unequal cell-array tuple counts return `-61860` | `Invalid: Cell Array Size Mismatch` | +| 2 | Preflight | Equal tuple counts create a scalar feature array with the Feature Attribute Matrix shape | `Valid: Uniform Phases` | +| 3 | Validation | Negative FeatureId returns `-5355` | `Invalid: Negative FeatureId` | +| 4 | Validation | FeatureId exceeds the feature-array range and returns `-5351` | `Invalid: FeatureId Out Of Bounds` | +| 5 | Validation | FeatureId bulk read fails and returns its error | *Not directly tested. No fixture injects a validator read failure.* | +| 6 | Input | Empty input skips validation reads and the cell loop | *Not directly tested. Every fixture has cells.* | +| 7 | Input | Cancellation stops processing before a chunk read | *Not directly tested. No fixture injects cancellation.* | +| 8 | Input | Full chunks and later chunks preserve phase order across chunk boundaries | *Not directly tested. The largest fixture has 32 cells, below both the validator and algorithm chunk sizes.* | +| 9 | Input | A partial chunk reads both cell arrays successfully | `Valid: Uniform Phases` | +| 10 | Input | Algorithm FeatureId read fails and returns its error | *Not directly tested. No fixture injects this read failure.* | +| 11 | Input | Cell-phase read fails and returns its error | *Not directly tested. No fixture injects this read failure.* | +| 12 | Assignment | Background cells with positive phases leave feature zero at zero | `Valid: Feature 0 Skip` | +| 13 | Assignment | Background cells with negative phases bypass phase validation | `Valid: Negative Cell Phase On Background Cell` | +| 14 | Assignment | A negative phase on a positive feature returns `-61861` | `Invalid: Negative Cell Phase` | +| 15 | Assignment | First cell of a feature sets its phase | `Valid: Uniform Phases` | +| 16 | Assignment | A later cell with the same phase causes no conflict | `Valid: Uniform Phases` | +| 17 | Assignment | A later cell with a different phase marks the feature and replaces its phase | `Valid: Mixed Phase Warning` | +| 18 | Assignment | A feature with no cells stays zero | `Valid: Gap Feature Stays Zero` | +| 19 | Output | Bulk output write stores the feature values | `Valid: Uniform Phases` | +| 20 | Output | Bulk output write fails and returns its error | *Not directly tested. No fixture injects a write failure.* | +| 21 | Warning | No conflicting features return no warnings | `Valid: Uniform Phases` | +| 22 | Warning | Up to 15 conflicting features produce one warning `-500` with their IDs | `Valid: Mixed Phase Warning` | +| 23 | Warning | More than 15 conflicting features limit the list and append the omitted count | `Valid: Truncated Mixed Feature Warning` — checks IDs 1 through 15 and the text `and 1 more` | +| 24 | Warning | More than one omitted feature selects the plural suffix | *Not directly tested. The truncation fixture omits exactly one feature.* | ## Test inventory -| TEST_CASE | Status | Notes | -|---|---|---| -| `(Valid Parameters)` | **Retired** | Circular oracle — `Computed_Phases` compared against `Cell Feature Data/Phases` in `6_6_stats_test_v2.dream3d`, which was produced by this filter's SIMPL predecessor. Replaced by 10 Class 1 Oracle tests. | -| `Valid: Uniform Phases` | Active | Case 1: 7 cells, 3 features, consistent phases. Paths 6, 7, 10. | -| `Valid: Feature 0 Skip` | Active | Case 2: 6 cells, background + 2 features. D1 assertion. Paths 4, 10. | -| `Valid: Mixed Phase Warning` | Active | Case 3: 2 cells, phase conflict, warning -500. Paths 8, 9a. | -| `Valid: Truncated Mixed Feature Warning` | Active | Case 4: 16 conflicting features, truncation suffix. Path 9b. | -| `Invalid: FeatureId Out Of Bounds` | Active | Case 5: error -5351. Path 1a. | -| `Invalid: Cell Array Size Mismatch` | Active | Case 6: preflight error -61860. Validates `preflightResult.outputActions`. Path 2. | -| `Invalid: Negative Cell Phase` | Active | Case 7: error -61861. Path 5. | -| `Invalid: Negative FeatureId` | Active | Case 8: error -5355. Path 1b. | -| `Valid: Gap Feature Stays Zero` | **New for V&V** | Case 9: features 2 and 4 have no cells; both stay `0`. Documents the zero-init contract D1 relies on. Path 11. | -| `Valid: Negative Cell Phase On Background Cell` | **New for V&V** | Case 10: negative phase on a `featureId == 0` cell is skipped, not an error. Locks in the scope of the -61861 guard. Paths 4, 5b. | -| `SIMPL Backwards Compatibility` | Kept, unchanged | `DYNAMIC_SECTION` over 6.4 and 6.5 SIMPL conversion fixtures; validates UUID, argument keys, and parameter conversion. | +All cases are in `src/Plugins/SimplnxCore/test/ComputeFeaturePhasesFilterTest.cpp`. All pass in CI. + +| Test case | Status | Notes | +|-----------|--------|-------| +| `SimplnxCore::ComputeFeaturePhasesFilter: Valid: Uniform Phases` | new-for-V&V | Seven cells produce `[0,1,2,1]`; checks four output tuples and no warnings. | +| `SimplnxCore::ComputeFeaturePhasesFilter: Valid: Feature 0 Skip` | new-for-V&V | Six cells produce `[0,1,2]`; checks background exclusion and no warnings (D1). | +| `SimplnxCore::ComputeFeaturePhasesFilter: Valid: Mixed Phase Warning` | new-for-V&V | Two cells give feature 1 phase 2; checks warning `-500` and exactly the listed ID 1 (D3). | +| `SimplnxCore::ComputeFeaturePhasesFilter: Valid: Truncated Mixed Feature Warning` | new-for-V&V | Sixteen conflicting features list IDs 1 through 15 and the text `and 1 more` (D3). Does not assert output phase values. | +| `SimplnxCore::ComputeFeaturePhasesFilter: Invalid: FeatureId Out Of Bounds` | new-for-V&V | FeatureId 5 with three feature tuples returns `-5351`. | +| `SimplnxCore::ComputeFeaturePhasesFilter: Invalid: Cell Array Size Mismatch` | new-for-V&V | Four FeatureId tuples and five phase tuples return preflight error `-61860`. | +| `SimplnxCore::ComputeFeaturePhasesFilter: Invalid: Negative Cell Phase` | new-for-V&V | Phase -1 on feature 1 returns `-61861` (D2). | +| `SimplnxCore::ComputeFeaturePhasesFilter: Invalid: Negative FeatureId` | new-for-V&V | FeatureId -1 returns `-5355`. | +| `SimplnxCore::ComputeFeaturePhasesFilter: Valid: Gap Feature Stays Zero` | new-for-V&V | Four cells use features 1 and 3; compares all five outputs with `[0,1,0,2,0]`. | +| `SimplnxCore::ComputeFeaturePhasesFilter: Valid: Negative Cell Phase On Background Cell` | new-for-V&V | Background phase -5 causes no error or warning; output is `[0,2]` (D1, D2). | +| `SimplnxCore::ComputeFeaturePhasesFilter: SIMPL Backwards Compatibility` | kept | `SIMPL 6.5 (UUID)` and `SIMPL 6.4 (Filter_Name)` variants check filter identity, comments and all four converted arguments. | ## Exemplar archive -No exemplar archive is used by any active test. Oracle data is inlined in test source. - -The `SIMPL Backwards Compatibility` test loads SIMPL JSON pipeline conversion fixtures from `test/simpl_conversion/6_5/ComputeFeaturePhasesFilter.json` and `test/simpl_conversion/6_4/ComputeFeaturePhasesFilter.json`; it does not load any `.dream3d` file. - -`6_6_stats_test_v2.tar.gz` was used exclusively by the retired `(Valid Parameters)` test (circular oracle — `DataContainer/Cell Feature Data/Phases` was generated by this filter's SIMPL predecessor). That archive is fully retired and must not be used as a correctness reference. - -## Deviations - -See `vv/deviations/ComputeFeaturePhasesFilter.md` for full entries and comparison run results. - -| ID | Summary | Affects `featurePhases[1..N]`? | Tested by | -|---|---|---|---| -| D1 | `featurePhases[0]` always 0 (background skip) | No — index 0 only | Cases 2, 9 | -| D2 | Negative cell phase → error -61861 | No — error path | Cases 7, 10 | -| D3 | Warning: consolidated list, 15-ID truncation | No — warning text only | Cases 3, 4 | - -**D1 downstream note:** Consumers reading `featurePhases[0]` must treat it as undefined. +- **Archive:** `6_6_stats_test_v2.tar.gz` — retired for this filter; the 10 inline fixtures replace its output comparison. +- **SHA512:** `e84999dec914d81efce4fc4237c49c9bf32e48381b1e79f58aa4df934f0d7606cd7a948f9a5e7b17a126a7944cc531b531cfdc70756ca3e2207b20734e089723` +- **Provenance:** [src/Plugins/SimplnxCore/vv/provenance/ComputeFeaturePhasesFilter.md](provenance/ComputeFeaturePhasesFilter.md) ---- +## Deviations from DREAM.3D v6.5.171 -## Note: ComputeFeaturePhasesBinaryFilter +See [vv/deviations/ComputeFeaturePhasesFilter.md](deviations/ComputeFeaturePhasesFilter.md) for the source-based differences, affected users and recommendations. -`ComputeFeaturePhasesBinaryFilter` (UUID `16010080-a913-443a-b5b3-bd43391fe3c0`, SIMPL equivalent `FindFeaturePhasesBinary` UUID `64d20c7b-697c-5ff1-9d1d-8a27b071f363`) is a distinct algorithm requiring its own V&V document at `vv/ComputeFeaturePhasesBinaryFilter.md`. Its existing execution test (`bin_feature_phases.tar.gz`) should be audited for circular-oracle issues. +| Deviation | Observed difference | +|-----------|---------------------| +| `ComputeFeaturePhasesFilter-D1` | SIMPLNX leaves feature zero at zero; DREAM.3D v6.5.171 stores the last background cell phase. | +| `ComputeFeaturePhasesFilter-D2` | SIMPLNX rejects negative phases on positive features; DREAM.3D v6.5.171 accepts them. | +| `ComputeFeaturePhasesFilter-D3` | SIMPLNX lists up to 15 conflicting feature IDs in one warning; DREAM.3D v6.5.171 lists a conflict count for every affected feature. | diff --git a/src/Plugins/SimplnxCore/vv/ComputeFeatureSizesFilter.md b/src/Plugins/SimplnxCore/vv/ComputeFeatureSizesFilter.md index e38f38020c..4e4dd9a564 100644 --- a/src/Plugins/SimplnxCore/vv/ComputeFeatureSizesFilter.md +++ b/src/Plugins/SimplnxCore/vv/ComputeFeatureSizesFilter.md @@ -1,112 +1,149 @@ -# V&V Report: ComputeFeatureSizes +# V&V Report: ComputeFeatureSizesFilter -| | | -|-----------|------------------| -| Plugin | SimplnxCore | +| | | +|--------|--------------| +| Plugin | SimplnxCore | | SIMPLNX UUID | `c666ee17-ca58-4969-80d0-819986c72485` | -| DREAM3D 6.5.171 equivalent | `FindSizes` — UUID `656f144c-a120-5c3b-bee5-06deab438588` | -| Verified commit | *pending* | +| SIMPLNX Human Name | Compute Feature Sizes | +| DREAM.3D v6.5.171 equivalent | `FindSizes` (Statistics plugin, UUID `656f144c-a120-5c3b-bee5-06deab438588`) | +| Verified commit | ** | | Status | COMPLETE | -| Sign-off | Nathan Young, June 10th, 2026. Second engineer: Michael A. Jackson , 2026-07-28. | +| Sign-off | Nathan Young, 2026-06-10; second engineer: Michael Jackson, 2026-07-28 | ## At a glance -| Aspect | Current state | -|------------------------|--------------------------| -| Algorithm relationship | **Port** of both `FindSizes::findSizesImage()` (ImageGeom) and `FindSizes::findSizesUnstructured()` (RectGridGeom) | -| Oracle | **Class 1 (Analytical)** — all test data inlined, hand-derived | -| Code paths | **14 of 19** exercised; 5 gaps (cancel ×2, INT32_MAX overflow ×2, other-geom no-op ×1) | -| Tests | **10 TEST_CASEs** — 9 pass; 1 (`[!shouldfail]`) fails-as-expected, pinning the open 2D divergence | -| External archive | None | -| Deviations | **2 active**, both A/B-verified 2026-06-27: `ComputeFeatureSizes-D1` (float64+Kahan vs naive summation → `Volumes`), `ComputeFeatureSizes-D2` (float64 `std::cbrt` vs float32 `powf` → `EquivalentDiameters`) — see deviations file | -| Open bugs | **1 open, pending design decision** (`Bug-1`): the 2D area formula multiplies all 3 spacings (PR #1590 slab convention, consistent with `ImageGeom::findElementSizes`) and diverges from legacy 6.5.171 (2 non-flat spacings) when the flat dimension's spacing ≠ 1. A legacy-matching fix was applied then deliberately reverted pending a team decision on the NX-wide 2D convention (see PR #1638 discussion and the deviations file's OPEN entry). | +| Aspect | Current state | +|--------|---------------| +| Algorithm Relationship | Port of `FindSizes` in DREAM.3D v6.5.171, with changes to precision, parallel execution, and 2D size calculation. | +| Oracle (confirmed) | Class 1 analytical counts, areas, volumes, and equivalent diameters. Three base fixtures and three output-block variants encode expectations in `test/ComputeFeatureSizesTest.cpp`. All pass in CI. The flat-spacing case is an expected failure (`ComputeFeatureSizes-D3`). | +| Code paths enumerated | 25 of 45 paths exercised with test assertions. Gaps include input read errors, count overflow, and several cancellation paths. | +| Tests today | 17 test cases: 15 visible and 2 hidden OOC cases. Coverage includes analytical values, output writes, invalid inputs, cell tuple counts, archive regression, and SIMPL conversion. | +| Exemplar archive | `6_6_stats_test_v2.tar.gz` supplies the Small IN100 input and three regression arrays. Analytical fixtures are constructed in C++. | +| Legacy comparison | Not run — runtime comparison artifacts are not available. Source comparison with DREAM.3D v6.5.171 identifies D1–D3. | +| Bug flags | `ComputeFeatureSizes-D3` | +| V&V phase | Analytical fixtures, source comparison, and test inventory are complete. Runtime comparison artifacts and a separate oracle-design review record are required. `ComputeFeatureSizes-D3` remains open with an expected-failure test. | ## Summary -`ComputeFeatureSizesFilter` produces three arrays per feature: `NumElements` (voxel count, `int32`), `Volumes`/`Areas` (float32), and `EquivalentDiameters` (ESD or ECD, float32). For ImageGeom it uses voxel count × voxel size; for RectGridGeom it sums per-cell element sizes per feature. Both paths are parallelized via `ParallelDataAlgorithm` + TBB `tbb::combinable`. All tests use Class 1 oracles (hand-constructed fixtures with first-principles expected values). Source-inspection confirms both geometry paths are ports of legacy `FindSizes`. Two precision deviations (`D1`, `D2`) — both confirmed by a direct A/B run against DREAM3D 6.5.171 — are documented below. A 2D-area divergence from legacy (`Bug-1`: the flat dimension's spacing is included in the per-voxel area, per the PR #1590 slab convention) was found during this V&V; a legacy-matching fix was applied and then deliberately reverted because it conflicted with the NX-wide 2D standardization from PR #1590 without a team decision. The divergence remains **open** and is pinned by a dedicated `[!shouldfail]` characterization test with a non-unit flat-dimension spacing; see the deviations file's OPEN entry for the two resolution options. +`ComputeFeatureSizesFilter` calculates feature voxel counts, areas or volumes, and equivalent diameters for Image and RectGrid geometries. Class 1 fixtures check the formulas, and the Small IN100 archive supplies a regression comparison. The V&V fixed two implementation defects; the 2D flat-spacing defect is not fixed. ## Algorithm Relationship -**Port** of `FindSizes` (both ImageGeom and RectGridGeom paths). +*Classification:* Port -- **ImageGeom** (`ProcessImageGeom`): voxel count × voxel volume/area → ESD/ECD. Port of `FindSizes::findSizesImage()` (legacy `execute()` dispatches to it via `findSizes()`). Formulas: `ESD = 2·∛(V / (4π/3))`; `ECD = 2·√(A / π)`. -- **RectGridGeom** (`ProcessRectGridGeom`): per-cell `ΔxΔyΔz` sizes, summed per feature. Port of `FindSizes::findSizesUnstructured()`. SIMPLNX departs in precision: float32 element sizes are promoted to float64 and Kahan summation is applied at two levels (per-thread and post-reduction). See deviation `D1`. - -Port-time additions not in DREAM3D 6.5.171: -- **Parallel execution** — `ParallelDataAlgorithm` + `tbb::combinable`; legacy was serial. -- **Execute-time FeatureId bounds check** — `ValidateFeatureIdsToFeatureAttributeMatrixIndexing`; legacy accessed out-of-bounds silently. -- **Preflight dimension guard** — rejects ImageGeom with two or more dimensions equal to 1. This is because a single empty dimension converts the calculation from Volume to Area and "1D" Images would not make sense to get an Area formula. -- **Tighter overflow guard** — SIMPLNX errors when a feature exceeds `INT32_MAX` voxels on both geometry paths. Legacy `findSizesImage` errored only above 2⁵³ (so counts between 2³¹ and 2⁵³ silently overflowed the int32 `NumElements`), and legacy `findSizesUnstructured` had no guard at all. -- **Narrower geometry scope** — the `GeometrySelectionParameter` allows only Image and RectGrid. Legacy `FindSizes` routed every non-image geometry (vertex/edge/triangle/quad/tet) through `findSizesUnstructured`; in SIMPLNX those are served by dedicated filters (e.g. `ComputeTriangleGeomVolumesFilter`). +*Evidence:* `SimplnxCoreLegacyUUIDMapping.hpp` maps `FindSizes` to this filter; both retain cell counting and circle/sphere formulas, with precision and execution changes documented in the sidecars. ## Oracle -**Class 1 (Analytical)** — all expected values are derived from first principles and asserted in `test/ComputeFeatureSizesTest.cpp`. +*Class:* 1 (Analytical). + +*Applied:* Count cells per feature, multiply by image cell size or sum RectGrid cell volumes, then use `2*sqrt(A/pi)` or `2*cbrt(3*V/(4*pi))`. + +*Encoded:* `src/Plugins/SimplnxCore/test/ComputeFeatureSizesTest.cpp::SimplnxCore::ComputeFeatureSizes: Valid: Image 2D` and five companion `Valid:` cases use three base fixtures; `SimplnxCore::ComputeFeatureSizesScanline: bounded output writes` adds three variants. All pass in CI. The flat-spacing case is an expected failure (`[!shouldfail]`) for `ComputeFeatureSizes-D3`. + +*Second-engineer review:* The report records Michael Jackson's sign-off on 2026-07-28. A separate record of the oracle-design review is required. -| Fixture | Geometry | Derived quantity | -|---|---|---| -| `Create2DImageDataStructure()` | 5×5×1 ImageGeom, spacing 20.2×0.1×1.0 | `voxelArea = 20.2 × 0.1 = 2.02` (flat-Z spacing excluded); areas = count × 2.02; ECDs from circle formula | -| `Create3DImageDataStructure()` | 5×5×5 ImageGeom, spacing 1.2×0.9×2.1 | `voxelVol = 1.2 × 0.9 × 2.1 = 2.268`; volumes = count × 2.268; ESDs from sphere formula | -| `CreateRectGridDataStructure()` | 4×4×4 non-uniform RectGrid | Per-cell `ΔxΔyΔz` summed per feature; step-by-step trace in provenance sidecar | +## Bugs found and fixed -Negative tests use degenerate inputs (out-of-bounds FeatureId; degenerate dims) and assert an error result is returned. +All defects in this table are fixed. The fixes will be in the DREAM3D-NX release after v7.4.2. DREAM3D-NX v7.4.2 contains the fix for `ComputeFeatureSizes-D4`. DREAM3D-NX v7.4.2 contains the fix for `ComputeFeatureSizes-D5`. -*Second-engineer review:* **Signed off by Michael A. Jackson, 2026-07-28** (PR #1638 review). Reviewed: the hand-derivations for all three fixtures and the tolerance choice (relative `1e-6` via Catch2 `Approx` — a few float32 ULPs of slack so the pins survive platform and TBB reduction-order differences). The V&V work was authored by Nathan Young, so the second-engineer review is independent of the author. +| Deviation | Defect | Affected released versions | Resolution | +|-----------|--------|----------------------------|------------| +| `ComputeFeatureSizes-D4` | RectGrid reduction applied Kahan compensation to the accumulated total instead of the incoming partial sum. | None. DREAM3D-NX v7.0.0 through v7.4.2 were not affected. DREAM.3D v6.5.171 was not affected. | The filter subtracts the compensation from the incoming partial sum before adding it to the accumulated total. | +| `ComputeFeatureSizes-D5` | Parallel workers could read nonresident FeatureIds without a store-residency scheduling guard. | None. DREAM3D-NX v7.0.0 through v7.4.2 were not affected. DREAM.3D v6.5.171 was not affected. | Direct execution checks FeatureIds residency before parallel scheduling. | ## Code path coverage -14 of 19 paths exercised. Source: `Algorithms/ComputeFeatureSizes.cpp`. - -| # | Path | Exercised by | -|---|---|---| -| 1 | Preflight: `emptyDimCount > 1` → error | `Invalid: Preflight Failure` (4 sub-checks) | -| 2 | Preflight: valid dims → pass | All 6 positive tests | -| 3 | Execute validate: FeatureId > numFeatures → error | `Invalid: Execution Failure` | -| 4 | Execute validate: passes → geometry dispatch | All 6 positive tests | -| 5 | Dispatch: ImageGeom → `ProcessImageGeom` | `Image 2D *`, `Image Stack 3D *` | -| 6 | Dispatch: RectGridGeom → `ProcessRectGridGeom` | `Rectilinear Grid *` | -| 7 | Dispatch: other geometry → no-op | *Not tested. `GeometrySelectionParameter` prevents this at runtime; gap acceptable.* | -| 8 | Image: any dim == 1 → area + ECD | `Image 2D *` | -| 9 | Image: all dims > 1 → volume + ESD | `Image Stack 3D *` | -| 10 | Image: voxelCount > `k_MaxVoxelCount` → error | *Not tested. Requires >2³¹ voxels in one feature; impractical. Gap acceptable.* | -| 11 | Image: `SaveElementSizes = false` | `Image 2D`, `Image Stack 3D` | -| 12 | Image: `SaveElementSizes = true` | `Image 2D with Element Sizes`, `Image Stack 3D with Element Size` | -| 13 | Image: `shouldCancel` in loop → early return | *Not tested. Cancel disregard would cause hangs in any test; low-value gap.* | -| 14 | RectGrid: `findElementSizes` → per-cell volumes available | `Rectilinear Grid *` | -| 15 | RectGrid: Kahan summation in `RectGridSummationImpl` + post-reduction | `Rectilinear Grid *` (non-uniform spacing exercises summation) | -| 16 | RectGrid: voxelCount > `k_MaxVoxelCount` → error | *Not tested. Same rationale as path 10.* | -| 17 | RectGrid: `SaveElementSizes = false` → `deleteElementSizes()` | `Rectilinear Grid` | -| 18 | RectGrid: `SaveElementSizes = true` → element sizes retained | `Rectilinear Grid with Element Size` | -| 19 | RectGrid: `shouldCancel` in loop → early return | *Not tested. Same rationale as path 13.* | +25 of 45 paths exercised with test assertions. Gaps include input read errors, count overflow, and cancellation during input processing. + +Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeFeatureSizes.cpp` (25 lines); `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeFeatureSizesDirect.cpp` (481 lines); `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeFeatureSizesScanline.cpp` (449 lines); `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeFeatureSizesFilter.cpp` (206 lines). + +Test names below omit the `SimplnxCore::ComputeFeatureSizes: ` prefix, except where `ComputeFeatureSizesScanline` is stated. The scenario harness selects Direct or Scanline according to the test configuration. + +| # | Phase | Path | Test case | +|---|-------|------|-----------| +| 1 | Preflight | Two or more Image dimensions are less than 2 → error. | `Invalid: Preflight Failure` — four dimension combinations. | +| 2 | Preflight | Valid Image dimensions → output actions. | `Valid: Image 2D`; `Valid: Image Stack 3D`. | +| 3 | Direct validation | FeatureId ≥ feature tuple count → error. | `Invalid: Execution Failure` — ID 10 with four feature tuples. | +| 4 | Direct validation | Valid FeatureIds → geometry dispatch. | `Valid: Image Stack 3D`; `Valid: Rectilinear Grid`. | +| 5 | Direct dispatch | Image geometry. | `Valid: Image 2D`; `Valid: Image Stack 3D`. | +| 6 | Direct dispatch | RectGrid geometry. | `Valid: Rectilinear Grid`. | +| 7 | Direct dispatch | Other geometry → no operation. | *Not directly tested. The selection parameter accepts only Image and RectGrid.* | +| 8 | Direct Image | One unit dimension → area and circular diameter. | `Valid: Image 2D` — flat spacing is 1. | +| 9 | Direct Image | All dimensions greater than 1 → volume and spherical diameter. | `Valid: Image Stack 3D`. | +| 10 | Direct Image | Feature count exceeds INT32_MAX in 2D or 3D → error. | *Not directly tested. No fixture supplies more than INT32_MAX cells in one feature.* | +| 11 | Direct Image | SaveElementSizes=false → no new element-size array. | `Valid: Image 2D`; `Valid: Image Stack 3D`. | +| 12 | Direct Image | SaveElementSizes=true → generate element sizes. | `Valid: Image 2D with Element Sizes`; `Valid: Image Stack 3D with Element Size`. | +| 13 | Direct Image | Cancel during counting, reduction, or output → return. | *Not directly tested. No test injects cancellation into Direct Image execution.* | +| 14 | Direct RectGrid | Generate cell volumes. | `Valid: Rectilinear Grid with Element Size`. | +| 15 | Direct RectGrid | Count cells and combine float64 compensated sums. | `Valid: Rectilinear Grid` — asserts three feature volumes and diameters. | +| 16 | Direct RectGrid | Feature count exceeds INT32_MAX → error. | *Not directly tested. No fixture supplies more than INT32_MAX cells in one feature.* | +| 17 | Direct RectGrid | SaveElementSizes=false → delete element sizes. | `Valid: Rectilinear Grid`. | +| 18 | Direct RectGrid | SaveElementSizes=true → retain element sizes. | `Valid: Rectilinear Grid with Element Size`. | +| 19 | Direct RectGrid | Cancel during counting, reduction, or output → return. | *Not directly tested. No test injects cancellation into Direct RectGrid execution.* | +| 20 | Direct validation | Negative FeatureId → error. | *Not directly tested. The invalid-ID fixture uses a positive ID above the limit.* | +| 21 | Direct scheduling | Nonresident FeatureIds → serial scheduling. | *Not directly tested. The real OOC fixtures require Scanline execution.* | +| 22 | Direct output | Background feature or unused feature → zero output. | *Not directly tested. The Direct analytical fixtures do not assert these entries.* | +| 23 | Scanline Image | 2D area and circular diameter; SaveElementSizes=false. | `SimplnxCore::ComputeFeatureSizesScanline: bounded output writes` — Image 2D. | +| 24 | Scanline Image | 3D volume and spherical diameter; SaveElementSizes=false. | `SimplnxCore::ComputeFeatureSizesScanline: bounded output writes` — Image 3D. | +| 25 | Scanline RectGrid | Cell-volume sums and spherical diameter; SaveElementSizes=false. | `SimplnxCore::ComputeFeatureSizesScanline: bounded output writes` — RectGrid. | +| 26 | Scanline element sizes | SaveElementSizes=true for 2D Image, 3D Image, and RectGrid. | `Valid: Image 2D with Element Sizes`; `Valid: Image Stack 3D with Element Size`; `Valid: Rectilinear Grid with Element Size`. | +| 27 | Scanline validation | Image FeatureId ≥ feature tuple count → error. | `Invalid: Execution Failure`. | +| 28 | Scanline validation | RectGrid FeatureId ≥ feature tuple count → error. | *Not directly tested. The invalid-ID fixture uses Image geometry only.* | +| 29 | Scanline validation | Negative Image or RectGrid FeatureId → error. | *Not directly tested. No negative-ID fixture exists in this test file.* | +| 30 | Scanline input | Image FeatureId bulk read fails → error. | *Not directly tested. The test stores inject output errors only.* | +| 31 | Scanline input | RectGrid FeatureId or element-size bulk read fails → error. | *Not directly tested. The test stores inject output errors only.* | +| 32 | Scanline input | Full 262,144-cell read followed by another input chunk. | *Not directly tested. The analytical fixtures expand feature outputs, not input cell counts.* | +| 33 | Scanline input | Cancel during Image or RectGrid input scan → return. | *Not directly tested. Cancellation is injected after an output block.* | +| 34 | Scanline output | Write a full block and a two-feature tail. | `SimplnxCore::ComputeFeatureSizesScanline: bounded output writes` — all three geometries. | +| 35 | Scanline output | NumElements, Volumes, or EquivalentDiameters write fails → stop. | `SimplnxCore::ComputeFeatureSizesScanline: output write failures stop later writes` — one variant per array. | +| 36 | Scanline output | Cancel after a completed block; empty flush → success. | `SimplnxCore::ComputeFeatureSizesScanline: cancellation preserves completed output blocks`. | +| 37 | Scanline output | Cancel with a partially filled block → flush completed features. | *Not directly tested. The cancellation fixture stops at a block boundary.* | +| 38 | Scanline output | Count exceeds INT32_MAX → flush then error. | *Not directly tested. No count-overflow fixture exists.* | +| 39 | Scanline output | Background and unused features stay zero. | `SimplnxCore::ComputeFeatureSizesScanline: bounded output writes` — background arrays and unused-feature counts and sizes. | +| 40 | Scanline stores | Real HDF5-OOC input and outputs; short and multiple output blocks. | `real OOC output writes preserve analytical values`; `real OOC multi-block output writes` — hidden cases. | +| 41 | Scanline dispatch | Other geometry → no operation. | *Not directly tested. The selection parameter accepts only Image and RectGrid.* | +| 42 | Element sizes | Image or RectGrid generation fails → return the error. | *Not directly tested. All geometry fixtures have valid element-size inputs.* | +| 43 | Element sizes | Reuse an existing element-size array. | *Not directly tested. No fixture explicitly checks reuse of an existing array.* | +| 44 | Output limits | No feature after background → empty output loop. | *Not directly tested. Every fixture has at least one non-background feature.* | +| 45 | Preflight | FeatureIds tuple count differs from the selected geometry cell count → error `-74771`. | `Geometry cell tuple count` — Image and RectGrid geometries with 12 cells reject 0, 11, and 13 tuples. | ## Test inventory +All names are in `src/Plugins/SimplnxCore/test/ComputeFeatureSizesTest.cpp`. Dynamic variants are listed in Notes. + | Test case | Status | Notes | -|---|---|---| -| `Valid: Image 2D` | kept | Class 1; 5×5×1, spacing 20.2×0.1×1.0 (flat-Z spacing 1.0); SaveElementSizes=false | -| `2D area excludes the flat-dimension spacing` | new-for-V&V | Class 1 characterization pin for the **open** Bug-1 divergence, tagged `[!shouldfail]`. 2×2 slab, non-flat spacings 2.0×3.0, flat-dimension spacing **5.0**; asserts the legacy value area == 24.0, while the current #1590 slab convention produces 120.0 — so it fails-as-expected until the 2D convention decision lands. `GENERATE`s all three flat orientations (X/Y/Z) so no axis-specific special case can satisfy it. | -| `Valid: Image 2D with Element Sizes` | kept | Same fixture as `Valid: Image 2D`; SaveElementSizes=true | -| `Valid: Image Stack 3D` | kept | Class 1; 5×5×5, spacing 1.2×0.9×2.1; SaveElementSizes=false | -| `Valid: Image Stack 3D with Element Size` | kept | Same fixture; SaveElementSizes=true | -| `Valid: Rectilinear Grid` | kept | Class 1; 4×4×4 non-uniform; SaveElementSizes=false; Kahan path exercised | -| `Valid: Rectilinear Grid with Element Size` | kept | Same fixture; SaveElementSizes=true | -| `Invalid: Execution Failure` | kept | FeatureId 10 in a 4-feature AM; asserts execute result invalid | -| `Invalid: Preflight Failure` | kept | 4 degenerate dim configs; asserts preflight result invalid | -| `SIMPL Backwards Compatibility` | kept | `DYNAMIC_SECTION` over SIMPL 6.4 + 6.5; validates UUID + arg-key + param-value decoding | -| `Legacy: Small IN100 Test` | retired | Real-data comparison against legacy-produced `6_6_stats_test_v2.dream3d` arrays. Retired because it was a legacy-output regression check (not an independent oracle) and the D1/D2 precision deviations intentionally change those exact values; the RectGrid A/B (deviations file) now provides the legacy comparison and the inline Class 1 fixtures provide the correctness oracle. | - -9 of 10 TEST_CASEs pass; the `[!shouldfail]` characterization test fails-as-expected (ctest green) while Bug-1 is unresolved. +|-----------|--------|-------| +| `SimplnxCore::ComputeFeatureSizesScanline: bounded output writes` | kept | Variants: Image 2D, Image 3D, RectGrid. Checks analytical values, one 262,144-feature block plus a two-feature tail, and no scalar writes. The 2D variant pins the current slab formula, not true area. | +| `SimplnxCore::ComputeFeatureSizesScanline: output write failures stop later writes` | kept | Variants: NumElements, Volumes, EquivalentDiameters. Injects each output write error and checks that later writes stop. | +| `SimplnxCore::ComputeFeatureSizesScanline: cancellation preserves completed output blocks` | kept | Cancels after the first complete output block. Checks completed values and zero values in the unwritten tail. | +| `SimplnxCore::ComputeFeatureSizes: real OOC output writes preserve analytical values` | kept | Hidden OOC case. Checks seven feature tuples on real HDF5-OOC stores; skips the body in an in-core build without that store. | +| `SimplnxCore::ComputeFeatureSizes: real OOC multi-block output writes` | kept | Hidden OOC case. Requires HDF5-OOC and checks values across an output-block boundary. | +| `SimplnxCore::ComputeFeatureSizes: Valid: Image 2D` | kept | Checks counts, areas, and circular diameters for three features; confirms that element sizes are absent. Float comparisons now use relative tolerance `1e-6` for platform and reduction-order variation. | +| `SimplnxCore::ComputeFeatureSizes: 2D area excludes the flat-dimension spacing` | new-for-V&V | Expected failure (`[!shouldfail]`). Variants: flat Z, flat X, flat Y. Expects area 24 with flat spacing 5; both algorithms calculate 120 (`ComputeFeatureSizes-D3`). | +| `SimplnxCore::ComputeFeatureSizes: Valid: Image 2D with Element Sizes` | kept | Checks the same 2D values and the presence of the element-size array. Float comparisons now use relative tolerance `1e-6` for platform and reduction-order variation. | +| `SimplnxCore::ComputeFeatureSizes: Valid: Image Stack 3D` | kept | Checks counts, volumes, and spherical diameters for three features; confirms that element sizes are absent. Float comparisons now use relative tolerance `1e-6` for platform and reduction-order variation. | +| `SimplnxCore::ComputeFeatureSizes: Valid: Image Stack 3D with Element Size` | kept | Checks the same 3D values and the presence of the element-size array. Float comparisons now use relative tolerance `1e-6` for platform and reduction-order variation. | +| `SimplnxCore::ComputeFeatureSizes: Valid: Rectilinear Grid` | kept | Checks counts, volumes, and spherical diameters on a nonuniform grid; confirms deletion of element sizes. Float comparisons now use relative tolerance `1e-6` for platform and reduction-order variation. | +| `SimplnxCore::ComputeFeatureSizes: Valid: Rectilinear Grid with Element Size` | kept | Checks the same RectGrid values and retention of element sizes. Float comparisons now use relative tolerance `1e-6` for platform and reduction-order variation. | +| `SimplnxCore::ComputeFeatureSizes: Invalid: Execution Failure` | kept | Uses FeatureId 10 with four feature tuples. Expects an execution error. | +| `SimplnxCore::ComputeFeatureSizes: Invalid: Preflight Failure` | kept | Checks rejection of dimensions (5,1,1), (1,5,1), (1,1,5), and (1,1,1). | +| `SimplnxCore::ComputeFeatureSizes: Legacy: Small IN100 Test` | kept | Loads `6_6_stats_test_v2.tar.gz` and compares all three generated arrays with archived arrays. This is a regression check, not an independent oracle. | +| `SimplnxCore::ComputeFeatureSizesFilter: SIMPL Backwards Compatibility` | kept | Variants: `SIMPL 6.5 (UUID)` and `SIMPL 6.4 (Filter_Name)`. Checks filter identity, empty comments, and seven converted parameter values. | +| `SimplnxCore::ComputeFeatureSizes: Geometry cell tuple count` | new-for-V&V | Image and RectGrid geometries have 12 cells. Rejects FeatureIds with 0, 11, or 13 tuples with error `-74771`; checks the array path and both counts in the message. Accepts 12 tuples in another Attribute Matrix with a different tuple shape. | ## Exemplar archive -None — all fixtures constructed in C++ at test time. Provenance in `vv/provenance/ComputeFeatureSizesFilter.md`. - -## Deviations from DREAM3D 6.5.171 - -Both deviations below were confirmed by a direct A/B run (2026-06-27), not source inspection alone: the exact RectGrid fixture was authored as a shared legacy `.dream3d` and run through stock DREAM3D 6.5.171, DREAM3D-NX, and a 6.5.172 proof-patch build. Applying **both** the D1 (summation) and D2 (ESD-evaluation) changes to legacy `findSizesUnstructured` made `Volumes` and `EquivalentDiameters` **bit-identical** to SIMPLNX; each change alone closed only its corresponding array. +- **Archive:** `6_6_stats_test_v2.tar.gz` +- **SHA512:** `e84999dec914d81efce4fc4237c49c9bf32e48381b1e79f58aa4df934f0d7606cd7a948f9a5e7b17a126a7944cc531b531cfdc70756ca3e2207b20734e089723` +- **Provenance:** `src/Plugins/SimplnxCore/vv/provenance/ComputeFeatureSizesFilter.md` -**`ComputeFeatureSizes-D1` (RectGridGeom → `Volumes`):** Per-feature volumes differ from `FindSizes::findSizesUnstructured()` output due to two precision improvements in SIMPLNX: (1) element sizes promoted from float32 to float64 before accumulation; (2) Kahan compensated summation. Note the Kahan compensator is a local reset on each TBB body invocation (the `combinable` per-thread volume persists, but the compensation term does not carry across chunk boundaries within a thread), so the accumulated-error reduction is per-chunk rather than the full O(ε_float64) a single continuous Kahan pass would give; the dominant improvement is the float64 accumulation. SIMPLNX is still more accurate than the legacy naive float32 sum. Users migrating from DREAM3D 6.5.171 should expect small shifts in per-feature volumes for RectGridGeom; largest for features with many cells on grids with high cell-volume variation. +## Deviations from DREAM.3D v6.5.171 -**`ComputeFeatureSizes-D2` (RectGridGeom → `EquivalentDiameters`):** Even with identical `Volumes`, the equivalent spherical diameter differs because legacy evaluates the cube root with `powf` (float32) on the float32-rounded volume, while SIMPLNX uses `std::cbrt` (float64) on the float64 volume. SIMPLNX is more accurate. The same `powf`/`sqrtf`-on-float32 pattern exists on the ImageGeom path; it was previously noted only as a non-flagged precision difference and is now captured by D2. +See `vv/deviations/ComputeFeatureSizesFilter.md` for the root cause, affected users, and recommendation for each deviation. Part 1 contains the following entries. -Full entry in `vv/deviations/ComputeFeatureSizesFilter.md`. +| Deviation | Observed difference | +|-----------|---------------------| +| `ComputeFeatureSizes-D1` | RectGrid feature volumes use float64 compensated sums instead of naive float32 sums. | +| `ComputeFeatureSizes-D2` | Equivalent diameters use float64 roots and unrounded feature sizes instead of the stored float32 sizes. | +| `ComputeFeatureSizes-D3` | A 2D image includes the flat spacing in its area value and then uses the circle diameter formula. DREAM.3D v6.5.171 excludes that spacing. | diff --git a/src/Plugins/SimplnxCore/vv/ComputeGroupingDensityFilter.md b/src/Plugins/SimplnxCore/vv/ComputeGroupingDensityFilter.md index 8d097d0982..e134274c2c 100644 --- a/src/Plugins/SimplnxCore/vv/ComputeGroupingDensityFilter.md +++ b/src/Plugins/SimplnxCore/vv/ComputeGroupingDensityFilter.md @@ -1,160 +1,112 @@ # V&V Report: ComputeGroupingDensityFilter -| | | -|-----------|------------------| -| Plugin | SimplnxCore | -| SIMPLNX UUID | ff46afcf-de32-4f37-98bc-8f0fd4b3c122 | -| DREAM3D 6.5.171 equivalent | `FindGroupingDensity` (SIMPL UUID `708be082-8b08-4db2-94be-52781ed4d53d`) — *unreleased pre-simplnx implementation* on `tuks188/DREAM3D` `feature/770_Grouping_Density`; never merged into `v6_5_171`. Served as the port source. | +| | | +|--------|--------------| +| Plugin | SimplnxCore | +| SIMPLNX UUID | `ff46afcf-de32-4f37-98bc-8f0fd4b3c122` | +| SIMPLNX Human Name | Compute Grouping Densities | +| DREAM.3D v6.5.171 equivalent | None. `FindGroupingDensity` exists in a local proof version only. | | Verified commit | ** | -| Status | **COMPLETE — 2026-05-27** | -| Sign-off | Michael Jackson (BlueQuartz Software), 2026-05-27 | +| Status | COMPLETE | +| Sign-off | Michael Jackson, 2026-05-27; second engineer: Nathan Young, 2026-06-09 | ## At a glance -| Aspect | Current state | -|------------------------|--------------------------| -| Algorithm Relationship | **Port** of legacy `FindGroupingDensity::execute()` from `tuks188/DREAM3D` `feature/770_Grouping_Density` (an un-merged feature branch on a contributor fork — never shipped in any DREAM3D 6.5.x release). Same SIMPL UUID retained. Six port-time deltas documented (`QVector→unordered_set` membership swap; runtime `if`→`if constexpr` specializations; conditional `checkedFeatureVolumes` allocation; added `m_ShouldCancel` + throttled progress feedback) — all performance/cancel/progress-only, none change output. **Caveat:** a small number of customers consumed a custom DREAM3D 6.5.x build that included this legacy filter, so the diff-explanation policy applies with a narrower migrant audience than usual. | -| Oracle (confirmed) | **Class 1 (Analytical) primary** — hand-derivation embedded in the Oracle section (set-union sums + ratio division on the 5-feature v2 hand-built fixture; 4-configuration expected-output table). **Class 4 (Invariant) companion** — sentinel `-1.0f` on empty parents; `density[0] == 0.0f`; `density[i] ≤ 1.0f`; `density[i] > 0 ∨ == -1.0f`; `CheckedFeatures` range — asserted inline. **Second-engineer review skipped** (recorded reason): 5-feature hand-built fixture arithmetic is high-school-level set-union + ratio; bit-identical cross-check against an independently-built legacy serves as external validation. | -| Code paths enumerated | 7 of 8 listed paths exercised; cancellation remains outside this scope. | -| Tests today | 7 registered cases plus one hidden HDF5 boundary case. Both serial CTest selections pass 7/7. | -| Exemplar archive | `compute_grouping_densities_v2.tar.gz` (SHA512 `3aaabb63…3784ff`, wired into `test/CMakeLists.txt`). **Replaces** v1 archive `compute_grouping_densities.tar.gz` (retired from this filter's tests). The v2 archive carries inline provenance: input file + all 4 legacy/SIMPLNX outputs + comparison script + comparison report + legacy/SIMPLNX pipelines + hand-review sign-off ReadMe. **Provenance sidecar:** `vv/provenance/ComputeGroupingDensityFilter.md` — records the four-hand authorship chain (Claude Opus 4.6 initial dataset → MAJ structural sign-off → Claude Opus 4.7 coverage review → Claude Opus 4.7 Python generator + compare scripts) and the v1→v2 circular-oracle disposition. | -| Legacy comparison | **Empirical A/B against locally-rebuilt legacy** `FindGroupingDensity` (a local build of the legacy DREAM3D 6.5 source with the `feature/770_Grouping_Density` sources pulled in). All 4 `(UseNonContiguousNeighbors, FindCheckedFeatures)` configurations produce **BIT-IDENTICAL** `GroupingDensities` and `CheckedFeatures` output between SIMPLNX and the rebuilt legacy. **0 deviations.** See `vv/deviations/ComputeGroupingDensityFilter.md` for the per-configuration result table, comparison method, build provenance, and the migration recommendation for the legacy-custom-build customers (*trust SIMPLNX — output is bit-identical*). | -| Bug flags | **None — clean Port.** Algorithm verified correct as-is; no SIMPLNX-side algorithmic bug surfaced or resolved. **Cleanup-only changes applied**: `[SimplnxReview]`→`[SimplnxCore]` test-tag fix (7 tests now use the correct `[SimplnxCore][ComputeGroupingDensityFilter]` tag pair); code-comment cleanup applied to algorithm + filter source (sentinel `-1.0f` documented inline; deleted-special-members trailing comments removed; throwaway-placeholder pattern explained; `FindDensitySpecializations` + `FindDensityGrouping` class docs added); tie-break behavior surfaced in the user-facing filter doc. | -| V&V phase | Historical status and sign-off retained. Section 4.3 recertification adds independent HDF5 boundary evidence and paired runtime checks. | +| Aspect | Current state | +|--------|---------------| +| Algorithm Relationship | Port of `FindGroupingDensity` from a local proof version. DREAM.3D v6.5.171 has no equivalent filter. | +| Oracle (confirmed) | Class 1 set-union sums and volume ratios, with Class 4 invariants, are encoded in four archive configurations and one empty-parent fixture. All pass in CI. | +| Code paths enumerated | 19 of 25 exercised. Gaps include cancellation, failed neighbor retrieval, rejected parent claims, an empty parent loop and the HDF5 boundary path. | +| Tests today | 8 test cases: one four-configuration archive comparison, one empty-parent case, four preflight rejections, one hidden HDF5 fixture and one SIMPL conversion case. | +| Exemplar archive | `compute_grouping_densities_v2.tar.gz` supplies inputs and outputs for all four option combinations. It replaces `compute_grouping_densities.tar.gz`. | +| Legacy comparison | Not run — DREAM.3D v6.5.171 has no equivalent filter. A local proof version matched all 4 option combinations bit for bit on 2026-05-27; the algorithm changed afterward. | +| Bug flags | None | +| V&V phase | COMPLETE. Analytical values, invariants, input rejection and parameter conversion are covered. | ## Summary -- This **Filter** computes a **Grouping Density** value for each **Parent Feature** in a hierarchical reconstruction. Hierarchical reconstructions involve more than one level of segmentation, creating a **Feature** to **Parent Feature** relationship (e.g., grains grouped into reconstructed parent grains). -- The filter was verified by generating a small data set that exercises each code path and combination of featureIds. The final calculations were done by hand and then verified by executing the filter. -- The result is that the filter generates the expected output values - +`ComputeGroupingDensityFilter` divides each parent volume by the summed volume of its assigned features and their unique selected neighbors. Class 1 expectations check all four option combinations, and Class 4 checks cover output bounds and the empty-parent sentinel. All pass in CI. ## Algorithm Relationship +*Classification:* Port -*Classification:* **Port** - -*Evidence:* The SIMPLNX algorithm at `Algorithms/ComputeGroupingDensity.cpp` (238 lines) is a line-by-line translation of the legacy `FindGroupingDensity::execute()` on `tuks188/DREAM3D` `feature/770_Grouping_Density` (469-line `.cpp` file; ~80-line algorithm body). Identical control flow (nested parent×feature loops + neighbor-list walks), identical sentinel (`-1.0f` when `totalCheckVolume == 0.0f`), identical density formula (`curParentVolume / totalCheckVolume`), and a preserved-from-legacy variable-name lineage (`totalCheckVolume` → `totalFeatureCheckVolume`, `checkedfeaturevolumes` → `checkedFeatureVolumes`). Same SIMPL UUID retained via `SimplnxCoreLegacyUUIDMapping.hpp` + SIMPL conversion fixture at `test/simpl_conversion/6_5/ComputeGroupingDensityFilter.json`. **Important caveat:** the legacy filter was never officially released in any DREAM3D 6.5.x — it lived on an un-merged feature branch on a contributor fork. However, a small number of important customers consumed a custom DREAM3D 6.5.x build that included this filter, and those customers have downstream data dependent on its output. Therefore the policy's diff-explanation purpose **does** apply to this filter, just with a narrower migrant audience than usual. The legacy comparison evidence is captured in `vv/deviations/ComputeGroupingDensityFilter.md` via an A/B run against a local rebuild of the legacy DREAM3D 6.5 source (with the feature-branch sources pulled in). Verification still requires an independent oracle (see Oracle section). - -*Port-time deltas that do not change output (defensible "Port" rather than "Minor changes"):* - -1. `QVector` totalCheckList (linear `.contains()`, O(n²) per parent) → `std::unordered_set` (O(1) membership, O(n) per parent) — performance, no behavior change. -2. Runtime `if (m_FindCheckedFeatures == true)` in the inner loop → `if constexpr (FindingCheckedFeatures)` template specialization — performance, no behavior change. -3. Runtime `for(k=0; k checkedfeaturevolumes(numfeatures, 0.0f)` always allocated → conditionally allocated only when `FindCheckedFeatures==true` — memory savings, no behavior change (legacy zeros were unread when the flag was false). -5. Added: `m_ShouldCancel` check in the outer parent loop (legacy has no cancel support). -6. Added: throttled per-parent progress feedback (legacy emits one terminal "Complete" message). - -*Material PRs since baseline (2025-10-01):* - -- **#1548** — "FILT: Compute Grouping Density filter added." (merge `30c9b1090`, 2026-02-25) — initial port from the legacy feature branch: filter + algorithm (.hpp/.cpp), docs (with worked example + 3 figures), `FromSIMPLJson()` conversion path, legacy UUID map entry, 431-line test file covering all 4 template specializations + 3 preflight error tests, exemplar archive. -- *(excluded — broad refactor)* #1588 — "ENH: SIMPL Backwards Compatibility Test Redesign" (merge `f854bb636`, 2026-04-22) — on the cross-cutting exclusion list; added only the per-filter SIMPL backwards-compat fixture at `test/simpl_conversion/6_5/ComputeGroupingDensityFilter.json`. No algorithm change. - -OOC source: `Algorithms/ComputeGroupingDensity.cpp` (238 lines). The new test targets the storage path documented below. +*Evidence:* `src/Plugins/SimplnxCore/src/SimplnxCore/SimplnxCoreLegacyUUIDMapping.hpp` maps `FindGroupingDensity` to this filter; the archive README identifies the port source as a local proof version, with the same set-union density formula. ## Oracle -*Class:* **1 (Analytical)** primary, **4 (Invariant)** companion. - -*Applied (Class 1 — Analytical):* Expected outputs are hand-derived from the input definition without reference to any DREAM3D implementation. For each parent index `i ≥ 1`: - -1. `assigned = {j : ParentIds[j] == i}` -2. `touched = assigned ∪ {nbr : nbr ∈ contiguousNL[j], j ∈ assigned}` (and additionally `∪ {nbr : nbr ∈ nonContiguousNL[j], j ∈ assigned}` when `UseNonContiguousNeighbors == true`) -3. `totalCheckVolume[i] = Σ Volumes[k] for k ∈ touched` -4. `GroupingDensities[i] = ParentVolumes[i] / totalCheckVolume[i]`, or `-1.0f` sentinel when `totalCheckVolume[i] == 0` - -For `CheckedFeatures[k]` (when `FindCheckedFeatures == true`): the parent with the largest `ParentVolumes` among the parents that touched feature `k` (last-writer-wins-on-greater-volume semantics in the algorithm). - -Hand-derivation on the v2 hand-built fixture (`Volumes = [0,10,20,15,25,30]`, `ParentIds = [0,1,1,1,2,2]`, `ParentVolumes = [0,45,55]`, contiguous chain `1↔2↔3↔4↔5`, non-contiguous pairs `1↔4` and `2↔5`): - -| Config (NC, CF) | Parent 1 touched | Σ Vol | `density[1]` | Parent 2 touched | Σ Vol | `density[2]` | -|---|---|---|---|---|---|---| -| (0, *) | {1,2,3,4} | 70 | `45/70` ≈ `0.6428571` | {3,4,5} | 70 | `55/70` ≈ `0.7857143` | -| (1, *) | {1,2,3,4,5} | 100 | `0.45` | {1,2,3,4,5} | 100 | `0.55` | - -CheckedFeatures derivations (when CF=1): NC=0 → `[0,1,1,2,2,2]` (parent 1 claims features {1,2}; parent 2 claims {3,4} as the larger-volume parent overriding parent 1's earlier claim, plus its own {5}). NC=1 → `[0,2,2,2,2,2]` (both parents touch all 5; parent 2 wins on every feature). Full derivation in `vv/comparisons/ComputeGroupingDensityFilter/README.md` and the v2 archive's `README.md`. +*Class:* 1 (Analytical), with 4 (Invariant). -*Applied (Class 4 — Invariant):* Derivable properties any valid output must satisfy, asserted inline in test code (`ComputeGroupingDensityTest.cpp` Exemplar A/B + Empty-parent edge case): +*Applied:* Sum each unique assigned or neighboring feature volume once, divide the parent volume by that sum, and select the largest positive-volume parent for each checked feature; a zero sum gives density `-1.0f`. -- `GroupingDensities[0] == 0.0f` (placeholder parent never touched) -- For `i ≥ 1`: `GroupingDensities[i] > 0.0f ∨ == -1.0f` (positive or sentinel) -- For `i ≥ 1`: `GroupingDensities[i] ≤ 1.0f` (totalCheckVolume always includes the parent's own features → ≥ ParentVolumes[i]) -- `CheckedFeatures[k] ∈ {0, …, numParents-1}` when produced; `CheckedFeatures[0] == 0` -- Empty-parent sentinel asserted directly: `REQUIRE(densities[i] == -1.0f)` when parent `i` has no assigned features +*Encoded:* `src/Plugins/SimplnxCore/test/ComputeGroupingDensityTest.cpp::SimplnxCore::ComputeGroupingDensityFilter: Exemplar A/B — all 4 configurations` checks four configurations; `Empty-parent edge case (-1.0f sentinel)` adds one fixture. All pass in CI. -*Encoded:* - -- **Class 1 (Analytical)**: `test/ComputeGroupingDensityTest.cpp::"Exemplar A/B — all 4 configurations"` — 4 fixtures (`NC0_CF0`, `NC0_CF1`, `NC1_CF0`, `NC1_CF1`). Bit-exact `CompareDataArrays` and `` against the v2 exemplar (`compute_grouping_densities_v2.tar.gz`) whose `GroupingDensities_*` and `CheckedFeatures_*` arrays equal the hand-derivation above to float32 precision. -- **Class 4 (Invariant)**: same test (inline invariant predicates run for all 4 fixtures); plus `test/ComputeGroupingDensityTest.cpp::"Empty-parent edge case (-1.0f sentinel)"` — 2 assertions for `density[1] == 1.0` (parent with assigned features) and `density[2] == -1.0f` (sentinel). - -6 fixture assertions total, all pass at the verified commit. - -*Second-engineer review:* **Skipped — recorded reason:** Class 1 derivation is set-union sums + ratio division on a 5-feature hand-built fixture (high-school arithmetic). External cross-validation was obtained via the independently-authored legacy `FindGroupingDensity` implementation (`tuks188/DREAM3D` `feature/770_Grouping_Density` sources rebuilt locally): the Phase 9 A/B comparison produced bit-identical agreement across all 4 configurations (see `vv/deviations/ComputeGroupingDensityFilter.md` and `compute_grouping_densities_v2/results/ab_comparison_report.txt`). Any oracle-derivation error would have surfaced as a Deviation. Formal second-engineer review of a 5-feature analytical oracle was not justified given this cross-check. +*Second-engineer review:* Skipped — the recorded rationale uses the five-feature analytical derivation and the local proof comparison as the cross-check. ## Bugs found and fixed -No new defect was found during this OOC recertification. Existing defect and deviation dispositions in this report and its sidecar are retained. +None. ## Code path coverage - -*7 of 8 paths exercised.* - -Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeGroupingDensity.cpp` - -The algorithm dispatches on two booleans, producing 4 template specializations of `FindDensityGrouping()`. Two additional runtime branches handle the empty-parent sentinel and cancellation. Preflight error paths are tested separately at the filter level. - -| Path | Test case | -|---|---| -| `UseNonContiguousNeighbors=true, FindCheckedFeatures=true` (full path, both neighbor lists + per-feature parent tracking) | `Exemplar A/B — all 4 configurations` (DYNAMIC_SECTION `NC1_CF1`) | -| `UseNonContiguousNeighbors=true, FindCheckedFeatures=false` (both neighbor lists, no per-feature parent tracking) | `Exemplar A/B — all 4 configurations` (DYNAMIC_SECTION `NC1_CF0`) | -| `UseNonContiguousNeighbors=false, FindCheckedFeatures=true` (contiguous neighbors only + per-feature parent tracking) | `Exemplar A/B — all 4 configurations` (DYNAMIC_SECTION `NC0_CF1`) | -| `UseNonContiguousNeighbors=false, FindCheckedFeatures=false` (contiguous neighbors only, no per-feature parent tracking) | `Exemplar A/B — all 4 configurations` (DYNAMIC_SECTION `NC0_CF0`) | -| Edge: `totalFeatureCheckVolume == 0.0f` for a parent → density sentinel `-1.0f` written at line 114 | `Empty-parent edge case (-1.0f sentinel)` | -| Cancellation: `m_ShouldCancel` checked inside the parent-id outer loop (line 76); early return without writing further densities | *Not directly tested — structurally covered; requires cancel-signal injection (low-value guard).* | -| Preflight errors: invalid / mismatched input array paths → `-15670`/`-15671`/`-15672`/`-15673` | 4 dedicated preflight-error tests (one per code) | -| HDF5 list chunk boundary | `real HDF5 neighbor-list chunk oracle` — features 2/3 cross three-tuple list chunks; exact 0.75 or 0.5 density and checked-feature claims | +19 of 25 paths exercised. The uncovered rows identify the remaining gaps. + +Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeGroupingDensity.cpp` (237 lines) and `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeGroupingDensityFilter.cpp` (290 lines). + +Test names below omit the common `SimplnxCore::ComputeGroupingDensityFilter: ` prefix. NC means Use Non-Contiguous Neighbors; CF means Find Checked Features. + +| # | Phase | Path | Test case | +|---|-------|------|-----------| +| 1 | Dispatch | NC=true, CF=true | `Exemplar A/B — all 4 configurations` — `NC1_CF1` checks densities and checked features | +| 2 | Dispatch | NC=true, CF=false | `Exemplar A/B — all 4 configurations` — `NC1_CF0` checks densities | +| 3 | Dispatch | NC=false, CF=true | `Exemplar A/B — all 4 configurations` — `NC0_CF1` checks densities and checked features | +| 4 | Dispatch | NC=false, CF=false | `Exemplar A/B — all 4 configurations` — `NC0_CF0` checks densities | +| 5 | Finalize | Zero checked volume gives density `-1.0f` | `Empty-parent edge case (-1.0f sentinel)` | +| 6 | Cancellation | Cancel before processing a parent | *Not directly tested. No fixture injects cancellation.* | +| 7 | Preflight | Feature tuple counts differ; reject with `-15671` | `Preflight Error - Feature tuple count mismatch (-15671)` — checks rejection | +| 8 | Preflight | Selected non-contiguous list has a different tuple count; reject with `-15672` | `Preflight Error - NonContiguousNL tuple count mismatch (-15672)` — checks rejection | +| 9 | Preflight | Feature volumes are outside an AttributeMatrix; reject with `-15673` | `Preflight Error - Volumes not in AttributeMatrix (-15673)` — checks rejection | +| 10 | Preflight | Parent volumes are outside an AttributeMatrix; reject with `-15670` | `Preflight Error - Parent Volumes not in AttributeMatrix (-15670)` — checks rejection | +| 11 | Finalize | Positive checked volume gives the parent-to-checked-volume ratio | `Exemplar A/B — all 4 configurations`; `Empty-parent edge case (-1.0f sentinel)` | +| 12 | Iteration | Skip parent and feature index zero | `Exemplar A/B — all 4 configurations` — asserts zero placeholder outputs | +| 13 | Iteration | Feature belongs to another parent; skip its neighbor traversal | `Exemplar A/B — all 4 configurations` — two parents with different volumes | +| 14 | Accumulation | Assigned feature is new to the set; add its volume | `Empty-parent edge case (-1.0f sentinel)` — empty neighbor lists | +| 15 | Accumulation | Assigned feature is already in the set; do not add its volume again | `Exemplar A/B — all 4 configurations` — chain neighbors include later assigned features | +| 16 | Accumulation | Neighbor is new to the set; add its volume | `Exemplar A/B — all 4 configurations` | +| 17 | Accumulation | Neighbor is already in the set; do not add its volume again | `Exemplar A/B — all 4 configurations` — reciprocal chain neighbors | +| 18 | Accumulation | Empty neighbor list; skip the neighbor loop | `Empty-parent edge case (-1.0f sentinel)` | +| 19 | Neighbor access | `getValue()` fails; return from the neighbor helper | *Not directly tested. No fixture injects a neighbor retrieval failure.* | +| 20 | Checked features | Assigned feature accepts a larger positive parent volume | `Exemplar A/B — all 4 configurations` — CF=true variants | +| 21 | Checked features | Neighbor accepts a larger positive parent volume | `Exemplar A/B — all 4 configurations` — parent 2 replaces parent 1 on shared features | +| 22 | Checked features | Assigned feature rejects a parent volume less than or equal to its current claim | *Not directly tested. No fixture makes this comparison false for a newly visited assigned feature.* | +| 23 | Checked features | Neighbor rejects a parent volume less than or equal to its current claim | *Not directly tested. No fixture gives an earlier claimant an equal or larger volume.* | +| 24 | Iteration | No parent after index zero; skip the parent loop | *Not directly tested. Every execution fixture has nonzero parent indices.* | +| 25 | Storage | Traverse neighbor lists across an HDF5 chunk boundary | *Not directly tested. The hidden `real HDF5 neighbor-list chunk oracle` requires the HDF5 store provider.* | ## Test inventory -| Test case | Status | Notes | -|---|---|---| -| `SimplnxCore::ComputeGroupingDensityFilter: Exemplar A/B — all 4 configurations` | new-for-V&V | DYNAMIC_SECTION over `NC0_CF0`/`NC0_CF1`/`NC1_CF0`/`NC1_CF1`; bit-exact `CompareDataArrays`/`` against the v2 exemplar (equals the Class 1 hand-derivation) + inline Class 4 invariants. Replaces 5 redundant pre-V&V tests (4 execution variants + 1 v1-exemplar consumer). | -| `SimplnxCore::ComputeGroupingDensityFilter: Empty-parent edge case (-1.0f sentinel)` | new-for-V&V | Class 4 — `density[1] == 1.0` (parent with features) and `density[2] == -1.0f` (empty-parent sentinel). | -| `SimplnxCore::ComputeGroupingDensityFilter: Preflight error -15670` | kept | Invalid/mismatched input array path. | -| `SimplnxCore::ComputeGroupingDensityFilter: Preflight error -15671` | kept | Invalid/mismatched input array path. | -| `SimplnxCore::ComputeGroupingDensityFilter: Preflight error -15672` | new-for-V&V | Closed a preflight coverage gap. | -| `SimplnxCore::ComputeGroupingDensityFilter: Preflight error -15673` | kept | Invalid/mismatched input array path. | -| `SimplnxCore::ComputeGroupingDensityFilter: SIMPL Backwards Compatibility` | kept | `DYNAMIC_SECTION` over SIMPL 6.5 conversion fixture; UUID + argument conversion only. | -| `real HDF5 neighbor-list chunk oracle` | new-for-V&V | Actual HDF5 input/output and independent boundary expectations; 143 assertions across the configured options. | - -Restructured 8 → 7 tests. All 7 use the `[SimplnxCore][ComputeGroupingDensityFilter]` tag (corrected from a -`[SimplnxReview]` mis-tag) and pass at the verified commit. +The seven ordinary cases are covered by the supplied Linux, macOS and Windows CI result. All pass in CI. -OOC recertification (2026-09-21): serial CTest passed 7/7 in each DREAM3DNX build. `test/ComputeGroupingDensityTest.cpp::real HDF5 neighbor-list chunk oracle` passed 143 assertions in the OOC binary. All four option combinations run on HDF5 numeric arrays and HDF5 NeighborLists with shape {3,1,3}. The current list factory stores each outer slice in a separate chunk. Feature 2 is the last tuple of chunk zero and feature 3 starts chunk one. Duplicate neighbor 4 counts once; optional neighbor 8 changes the denominator from 16 to 24, giving exact densities 12/16=0.75 and 12/24=0.5. Parent 2 remains -1. The legacy baseline is still the documented custom source build, not a stock 6.5.171 importer. Upstream/develop oracle assertions and tolerances remain intact. No fresh legacy binary comparison is claimed. +| Test case | Status | Notes | +|-----------|--------|-------| +| `SimplnxCore::ComputeGroupingDensityFilter: Exemplar A/B — all 4 configurations` | new-for-V&V | Checks `NC0_CF0`, `NC0_CF1`, `NC1_CF0` and `NC1_CF1`. Compares densities with absolute tolerance `0.0001` and checked features exactly; also checks placeholder values, density bounds, parent-ID bounds and tuple dimensions. Replaces the v1 archive consumer and four redundant execution cases. | +| `SimplnxCore::ComputeGroupingDensityFilter: Empty-parent edge case (-1.0f sentinel)` | new-for-V&V | Constructs two assigned features and one empty parent. Checks density 1 with relative tolerance `0.0001`, density `-1.0f` exactly and output tuple dimensions. | +| `SimplnxCore::ComputeGroupingDensityFilter: Preflight Error - Feature tuple count mismatch (-15671)` | kept | Supplies ten ParentIds tuples and six volume/list tuples. Asserts invalid preflight, without checking the error code. | +| `SimplnxCore::ComputeGroupingDensityFilter: Preflight Error - NonContiguousNL tuple count mismatch (-15672)` | new-for-V&V | Supplies four non-contiguous list tuples and six feature tuples. Asserts invalid preflight, without checking the error code. | +| `SimplnxCore::ComputeGroupingDensityFilter: Preflight Error - Volumes not in AttributeMatrix (-15673)` | kept | Places feature volumes directly under the geometry. Asserts invalid preflight, without checking the error code. | +| `SimplnxCore::ComputeGroupingDensityFilter: Preflight Error - Parent Volumes not in AttributeMatrix (-15670)` | kept | Places parent volumes directly under the geometry. Asserts invalid preflight, without checking the error code. | +| `SimplnxCore::ComputeGroupingDensityFilter: real HDF5 neighbor-list chunk oracle` | new-for-V&V | Hidden fixture. Encodes all four option combinations, duplicate-neighbor suppression, densities `0.75` or `0.5`, the empty-parent sentinel, exact checked-feature IDs and absence of the disabled output. | +| `SimplnxCore::ComputeGroupingDensityFilter: SIMPL Backwards Compatibility` | kept | The `SIMPL 6.5 (UUID)` dynamic section checks filter identity, empty comments and nine converted arguments. It does not execute the algorithm. | ## Exemplar archive -- **Archive:** `compute_grouping_densities_v2.tar.gz` — **replaces** v1 `compute_grouping_densities.tar.gz` (retired from this filter's tests as a circular oracle). Carries inline provenance: input file + all 4 legacy/SIMPLNX outputs + comparison script + report + both pipelines + hand-review sign-off ReadMe. +- **Archive:** `compute_grouping_densities_v2.tar.gz` - **SHA512:** `3aaabb63c4fa16f7fa192ae4ee9dbba9394ec7f1cd19aff55e399a624d495a3a778c7f6f282911f681e85cea99e4c6d15344274e9107f337af7d4a19f93784ff` -- **Provenance:** `src/Plugins/SimplnxCore/vv/provenance/ComputeGroupingDensityFilter.md` — records the authorship chain and the v1→v2 circular-oracle disposition. - -## Deviations from DREAM3D 6.5.171 - -**Baseline note (this filter):** the legacy equivalent is the pre-SIMPLNX `FindGroupingDensity` on `tuks188/DREAM3D` `feature/770_Grouping_Density`, which was never shipped in any DREAM3D 6.5.x release (a few customers consumed it via a custom 6.5.x build) — so "DREAM3D 6.5.171" here denotes that feature-branch source rebuilt locally, not the shipped 6.5.171 baseline. See the Algorithm Relationship section for context. - -**No deviations observed.** Runtime A/B comparison run on the -`compute_grouping_densities_v2.tar.gz` fixture: all 4 -`(UseNonContiguousNeighbors, FindCheckedFeatures)` combinations of -`ComputeGroupingDensityFilter` (SIMPLNX) and `FindGroupingDensity` (a local -build of the legacy DREAM3D 6.5 source with the feature-branch sources) produced -**bit-identical** `GroupingDensities` and `CheckedFeatures` output. See -`vv/deviations/ComputeGroupingDensityFilter.md` for the full per-configuration -result table, comparison method, build provenance, and the migration -recommendation for legacy-custom-build customers (*trust SIMPLNX, output is -bit-identical*). - -| Fixture | `compute_grouping_densities_v2.tar.gz` | -|---|---| -| SHA512 | `3aaabb63c4fa16f7fa192ae4ee9dbba9394ec7f1cd19aff55e399a624d495a3a778c7f6f282911f681e85cea99e4c6d15344274e9107f337af7d4a19f93784ff` | -| Driver script | `compute_grouping_densities_v2compare_outputs.py` | +- **Provenance:** `src/Plugins/SimplnxCore/vv/provenance/ComputeGroupingDensityFilter.md` + +## Deviations from DREAM.3D v6.5.171 + +Not applicable: DREAM.3D v6.5.171 has no equivalent filter. There are no Part 1 entries in `vv/deviations/ComputeGroupingDensityFilter.md`. + +| Deviation | Observed difference | +|-----------|---------------------| diff --git a/src/Plugins/SimplnxCore/vv/ComputeNeighborhoodsFilter.md b/src/Plugins/SimplnxCore/vv/ComputeNeighborhoodsFilter.md index 0d1bbc2d56..3a2119fcf3 100644 --- a/src/Plugins/SimplnxCore/vv/ComputeNeighborhoodsFilter.md +++ b/src/Plugins/SimplnxCore/vv/ComputeNeighborhoodsFilter.md @@ -1,121 +1,114 @@ # V&V Report: ComputeNeighborhoodsFilter -| | | -|-----------|------------------| -| Plugin | SimplnxCore | -| SIMPLNX UUID | `924c10e3-2f39-4c08-9d7a-7fe029f74f6d` | -| SIMPLNX Human Name | Compute Feature Neighborhoods | -| DREAM3D 6.5.171 equivalent | `FindNeighborhoods` (SIMPL UUID `697ed3de-db33-5dd1-a64b-04fb71e7d63e`) — `Source/Plugins/Statistics/StatisticsFilters/FindNeighborhoods.{h,cpp}` | -| Verified commit | ** | -| Status | COMPLETE — 2026-07-16 | -| Sign-off | Michael Jackson — 2026-07-16 | +| | | +|--------|--------------| +| Plugin | SimplnxCore | +| SIMPLNX UUID | `924c10e3-2f39-4c08-9d7a-7fe029f74f6d` | +| SIMPLNX Human Name | Compute Feature Neighborhoods | +| DREAM.3D v6.5.171 equivalent | `FindNeighborhoods` (Statistics plugin, SIMPL UUID `697ed3de-db33-5dd1-a64b-04fb71e7d63e`) | +| Verified commit | ** | +| Status | COMPLETE | +| Sign-off | Michael Jackson, 2026-07-02; second engineer: Nathan Young, 2026-07-10 | ## At a glance -| Aspect | Current state | -|------------------------|--------------------------| -| Algorithm Relationship | **Rewrite** of legacy `FindNeighborhoods`. Legacy tested an axis-aligned box in normalized bin-space with a per-feature reach; NX tests a true Euclidean sphere. A real **NX regression is resolved** (see Bug flags). A new "Search Radius (microns)" mode and removal of the unused Feature Phases input were also added. | -| Oracle (confirmed) | **Class 1 (Analytical) + Class 4 (Invariant).** Two hand-built synthetic fixtures with exact neighbor counts (microns mode; per-feature multiples mode incl. an asymmetry case), plus count==list-size and symmetry/asymmetry invariants. Encoded in `test/ComputeNeighborhoodsTest.cpp`; all pass. | -| Code paths enumerated | 14 of 15 exercised; 1 uncovered (the cancel-signal path, which requires cancel-signal injection). | -| Tests today | 5 registered cases plus one hidden HDF5 boundary case. Both serial CTest selections pass 5/5. | -| Exemplar archive | **None.** All oracles are inline analytical values. The legacy comparison used the shared `6_6_stats_test_v2.tar.gz` Small IN100 stats dataset as input only (not a unit-test exemplar for this filter). | -| Legacy comparison | **Run** on Small IN100 (`6_6_stats_test_v2.dream3d`, 620 features, mult=1) via 6.5.171 `PipelineRunner` vs `nxrunner`. After the fix, NX correlates **0.894** with legacy and finds **50.8%** as many neighbors — exactly the sphere/box volume ratio. One documented deviation (D1); phases removal (D2). | -| Bug flags | **Resolved NX regression** (not a legacy bug; PR #1485 landed 2026-01-08, so **7.4.1 is the only affected release**): the prior NX rewrite (PR #1485) introduced a `÷2` factor and a global (vs per-feature) radius, making the default `mult=1` find ~37× fewer neighbors than legacy (mean 0.29 vs 10.93). Fixed by restoring a per-feature Euclidean radius. | -| V&V phase | Historical status and sign-off retained. Section 4.3 recertification adds independent HDF5 boundary evidence and paired runtime checks. | +| Aspect | Current state | +|--------|---------------| +| Algorithm Relationship | Rewrite of `FindNeighborhoods` in DREAM.3D v6.5.171. SIMPLNX uses a Euclidean sphere with a per-feature or absolute radius instead of a box in integer bin coordinates. | +| Oracle (confirmed) | Class 1 counts and Class 4 invariants use two fixtures in `test/ComputeNeighborhoodsTest.cpp`. All pass in CI. | +| Code paths enumerated | 16 of 21 exercised by the five ordinary test cases. Gaps cover cancellation, empty input, background-only input, and external list storage. | +| Tests today | Six test cases: two analytical, two preflight, one SIMPL conversion, and one hidden HDF5 fixture. The five ordinary cases cover both radius modes and input validation. | +| Exemplar archive | None. Tests construct synthetic inputs and use inline analytical expectations. | +| Legacy comparison | Run against DREAM.3D v6.5.171 on 2026-10-22: 1 of 3 cases match; the differences come from ComputeNeighborhoodsFilter-D1. A local proof version with those fixes matches DREAM3D-NX in 3 of 3 cases, using constant legacy diameters for the DREAM3D-NX-only absolute-radius case. | +| Bug flags | None | +| V&V phase | Verification records, second-engineer review and the legacy comparison are complete. | ## Summary -`ComputeNeighborhoodsFilter` counts, for each feature, how many other features have their centroid within a search radius of that feature's centroid. Verification used a **Class 1 analytical oracle** (hand-built synthetic fixtures with exact, hand-derived neighbor counts for both the per-feature "Multiples" mode and the absolute "Search Radius (microns)" mode) together with **Class 4 invariants** (count == neighbor-list size; symmetry in microns mode; per-feature asymmetry in multiples mode). Reconciling SIMPLNX against this independent oracle — and then against legacy DREAM3D 6.5.171 on Small IN100 — surfaced a real regression in the prior NX rewrite: the search radius had been changed to `avgDiameter × mult ÷ 2` (a single global radius), which at the default `mult=1` found ~37× fewer neighbors than legacy. The fix restores a **per-feature Euclidean radius** `equivalentDiameter[i] × mult`, preserving the legitimate box→sphere improvement of the rewrite while recovering legacy's per-feature scale (post-fix correlation 0.894 with legacy; the residual 2× count difference is the geometrically-correct sphere-vs-box volume ratio). +`ComputeNeighborhoodsFilter` counts and lists feature centroids within each feature's search radius. Class 1 fixtures check exact counts, and Class 4 invariants check list sizes and symmetric or asymmetric membership. The V&V fixed the global-radius regression and background-feature processing; DREAM3D-NX v7.4.2 contains both fixes. ## Algorithm Relationship -*Classification:* **Rewrite** ~~| Port | Minor changes | New filter~~ +*Classification:* Rewrite -*Evidence:* Same SIMPL UUID retained (`697ed3de-db33-5dd1-a64b-04fb71e7d63e`; SIMPL 6.4/6.5 conversion fixtures at `test/simpl_conversion/6_*/ComputeNeighborhoodsFilter.json`). The core inclusion test is fundamentally different from legacy, so this is a rewrite whose outputs diverge from legacy by design — defended in the Deviations file. - -*History (why this is a rewrite with a mid-flight fix):* - -1. The original NX port (pre-#1485) was a faithful translation of legacy: per-feature `criticalDistance[i] = eqDiam[i]·mult / aveDiam`, compared against integer bin differences on each axis (an axis-aligned **box** in normalized bin space). -2. **PR #1485 ("BUG: New implementation")** correctly replaced the bin-membership proxy with a true **Euclidean distance** test — a genuine improvement — but in doing so redefined the radius as a single **global** `avgDiameter·mult ÷ 2`. The `÷2` and the global (rather than per-feature) radius were not present in legacy. The hand-verification used `mult=3.0`, which masked the effect; at the default `mult=1.0` the filter found almost no neighbors. -3. **This V&V** restored the per-feature Euclidean radius `equivalentDiameter[i]·mult` (no `÷2`), keeping the box→sphere improvement. The `avgDiameter` is now used only to size the internal spatial-hash grid. - -*Port-time deltas vs legacy (current state):* - -1. **Box → sphere.** Legacy: `|Δbin| < cd` on each axis (Chebyshev box in bin space). NX: `distSq ≤ radius²` (Euclidean sphere in physical units). This is the intended correctness improvement; it is the sole remaining source of numeric divergence (D1). -2. **Per-feature radius retained.** Both legacy and current NX scale the reach by each feature's own equivalent diameter, so large features have large neighborhoods and the relation is asymmetric. -3. **Feature Phases removed.** Legacy required a `FeaturePhases` input but never used it in the computation; NX removes the parameter (D2). -4. **New "Search Radius (microns)" mode.** No legacy equivalent; lets the user supply an absolute radius and does not require Equivalent Diameters. - -OOC source: `Algorithms/ComputeNeighborhoods.cpp` (320 lines). The new test targets the storage path documented below. +*Evidence:* `src/Plugins/SimplnxCore/src/SimplnxCore/SimplnxCoreLegacyUUIDMapping.hpp` maps `FindNeighborhoods` to this filter; the legacy source compares integer bin differences, while `ComputeNeighborhoods.cpp` compares squared Euclidean distance with each feature's squared radius. ## Oracle -*Class:* **1 (Analytical)** primary, **4 (Invariant)** companion. +*Class:* 1 (Analytical), with 4 (Invariant) companions. -*Applied:* Minimal synthetic geometries with hand-placed centroids (and, for multiples mode, hand-chosen equivalent diameters) make the expected neighbor counts derivable on paper. Microns mode: a constant radius of 3.5 on seven features gives counts `{0,1,2,1,1,0,1}`, including a pair (features 4 and 6) separated by exactly the search radius to pin the inclusive `distSq ≤ radius²` boundary. Multiples mode: `eqDiam = {6,2,2,2,2}`, `mult=1` gives per-feature radii `{6,2,2,2,2}`; the fixture is arranged so a large feature reaches a small one that does not reach back, yielding counts `{3,0,0,1,0}` and a checkable asymmetry. Both fixtures assert index 0 as well: the background feature 0 must end with a zero count and an empty list (the multiples fixture makes this discriminating by placing feature 0's centroid coincident with feature 1's). Class 4 invariants (`Neighborhoods[i] == NeighborList[i].size` for **every** index including 0, symmetry in microns mode, targeted per-feature asymmetry in multiples mode) are asserted alongside the exact values. +*Applied:* Hand-placed centroids give counts `{0,1,2,1,1,0,1}` at radius 3.5 and `{0,3,0,0,1,0}` with per-feature diameters; list sizes, background exclusion, boundary inclusion, and symmetric or asymmetric membership are asserted. -*Encoded:* `test/ComputeNeighborhoodsTest.cpp::ComputeNeighborhoods_SyntheticOracle` (microns) and `::ComputeNeighborhoods_MultiplesAnalyticalOracle` (per-feature). All pass at the verified commit. +*Encoded:* `src/Plugins/SimplnxCore/test/ComputeNeighborhoodsTest.cpp::SimplnxCore::ComputeNeighborhoods_SyntheticOracle` and `SimplnxCore::ComputeNeighborhoods_MultiplesAnalyticalOracle` — two fixtures. All pass in CI. -*Second-engineer review:* **Signed off by Michael Jackson (technical authority), 2026-07-16.** +*Second-engineer review:* Nathan Young, 2026-07-10. ## Bugs found and fixed -No new defect was found during this OOC recertification. Existing defect and deviation dispositions in this report and its sidecar are retained. +All defects in this table are fixed. DREAM3D-NX v7.4.2 contains the fixes for `ComputeNeighborhoodsFilter-D4` and `ComputeNeighborhoodsFilter-D5`. + +| Deviation | Defect | Affected released versions | Resolution | +|-----------|--------|----------------------------|------------| +| `ComputeNeighborhoodsFilter-D4` | A single radius of `averageDiameter * multiplier / 2` replaced each feature's own radius and changed neighbor membership. | DREAM3D-NX v7.4.1. DREAM.3D v6.5.171 was not affected. | Multiples mode uses `equivalentDiameter[i] * multiplier`; the average diameter sizes the search bins only. | +| `ComputeNeighborhoodsFilter-D5` | Background feature 0 could search for neighbors and obtain a nonzero count, while its output list stayed empty. | DREAM3D-NX v7.4.1. DREAM.3D v6.5.171 was not affected. | Search sources and candidates exclude feature 0; its count is zero and its list is empty. | ## Code path coverage -*14 of 15 paths exercised.* - -Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeNeighborhoods.cpp` (320 lines) + `ComputeNeighborhoodsFilter.cpp` preflight. - -Logical phases: **(a) preflight validation/actions**, **(b) radius + bin setup**, **(c) parallel per-feature spatial scan**, **(d) neighbor-list finalize**. - -| # | Phase | Path | Test case | -|----|-------|------|-----------| -| 1 | (a) | Multiples mode, `multiples ≤ 0` → error `-5732` | `ComputeNeighborhoods_InvalidSearchRadius` (multiples section, code asserted) | -| 2 | (a) | Microns mode, `searchRadius ≤ 0` → error `-5733` | `ComputeNeighborhoods_InvalidSearchRadius` (microns section, code asserted) | -| 3 | (a) | Multiples mode, tuple mismatch (eqDiam vs centroids) → error `-5730` | `ComputeNeighborhoods_InvalidSearchRadius` (tuple-mismatch section, code asserted) | -| 4 | (a) | Centroids parent is not an Attribute Matrix → error `-5731` | `ComputeNeighborhoods_InvalidSearchRadius` (loose-centroids section, code asserted) | -| 5 | (a) | Report Input Image Geometry Info preflight value (both modes) | `..._SearchRadiusPreflightInfo` | -| 6 | (a) | Microns sub-voxel radius → warning `-5734` | `..._SearchRadiusPreflightInfo` (sub-voxel section) | -| 7 | (a) | Microns oversized radius → warning `-5735` | `..._SearchRadiusPreflightInfo` (oversized section) | -| 8 | (b) | `SearchRadiusType == 0` → per-feature radii `eqDiam[i]·mult`, binSize = avgDiameter (excl. feature 0) | `..._MultiplesAnalyticalOracle` | -| 9 | (b) | `SearchRadiusType == 1` → constant radii = searchRadius (feature 0 gets none), binSize = searchRadius | `..._SyntheticOracle` | -| 10 | (c) | Neighbor within radius → `updateNeighborHood(i, j)` | both analytical oracles | -| 11 | (c) | Per-feature asymmetry (radius varies by feature) | `..._MultiplesAnalyticalOracle` (asymmetry assertions) | -| 12 | (c) | Self-skip (`j == i`) and background exclusion — feature 0 is excluded both as a search **source** (`Range(1, N)`) and as a **candidate** (`binToFeatures` built from feature 1) | Explicitly asserted in both oracles: `Neighborhoods[0] == 0` and `NeighborhoodList[0]` empty; the multiples fixture places feature 0's centroid coincident with feature 1's so a regression here fails the assertion | -| 13 | (c) | `shouldCancel` → early return from the parallel scan | *Not directly tested. Requires cancel-signal injection.* | -| 14 | (c) | Inclusive boundary: `distSq == radiusSq` counts as a neighbor | `..._SyntheticOracle` (features 4 and 6 at exactly radius distance, exactly representable in float32) | -| 15 | Storage | HDF5 NeighborList output crosses three-tuple chunks | `real HDF5 list-chunk oracle` — exact directed lists and counts in both radius modes | +16 of 21 paths exercised. Each uncovered row states the required fixture or signal. + +Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeNeighborhoods.cpp` (322 lines) and `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeNeighborhoodsFilter.cpp` (269 lines). + +Test names below omit the `SimplnxCore::` prefix. The two analytical cases are `ComputeNeighborhoods_SyntheticOracle` and `ComputeNeighborhoods_MultiplesAnalyticalOracle`. + +| # | Phase | Path | Test case | +|---|-------|------|-----------| +| 1 | Preflight | Multiples mode, multiplier ≤ 0 → error `-5732` | `ComputeNeighborhoods_InvalidSearchRadius` — zero multiplier | +| 2 | Preflight | Microns mode, radius ≤ 0 → error `-5733` | `ComputeNeighborhoods_InvalidSearchRadius` — zero radius | +| 3 | Preflight | Diameter and centroid tuple counts differ → error `-5730` | `ComputeNeighborhoods_InvalidSearchRadius` — tuple mismatch | +| 4 | Preflight | Centroids parent is not an Attribute Matrix → error `-5731` | `ComputeNeighborhoods_InvalidSearchRadius` — loose centroids | +| 5 | Preflight | Report geometry information; accept a radius without warnings | `ComputeNeighborhoods_SearchRadiusPreflightInfo` — reasonable radius | +| 6 | Preflight | Microns radius is below the minimum voxel spacing → warning `-5734` | `ComputeNeighborhoods_SearchRadiusPreflightInfo` — sub-voxel radius | +| 7 | Preflight | Microns radius exceeds the largest geometry extent → warning `-5735` | `ComputeNeighborhoods_SearchRadiusPreflightInfo` — oversized radius | +| 8 | Setup | Multiples mode computes per-feature radii and averages non-background diameters for bin size | `ComputeNeighborhoods_MultiplesAnalyticalOracle` | +| 9 | Setup | Microns mode uses one radius and does not require Equivalent Diameters | `ComputeNeighborhoods_SyntheticOracle` | +| 10 | Search | A candidate lies within the radius → increment count and append its ID | Both analytical cases | +| 11 | Search | Different per-feature radii permit asymmetric membership | `ComputeNeighborhoods_MultiplesAnalyticalOracle` — feature 1 lists feature 2, but feature 2 does not list feature 1 | +| 12 | Search | Skip self and exclude background as a source and candidate | Both analytical cases — exact counts include background; feature 0 coincides with feature 1 | +| 13 | Search | Cancel during the per-feature scan → return | *Not directly tested. Requires cancel-signal injection during the scan.* | +| 14 | Search | Squared distance equals squared radius → include the candidate | `ComputeNeighborhoods_SyntheticOracle` — features 4 and 6 are exactly 3.5 apart | +| 15 | Storage | External NeighborList output crosses a three-tuple chunk boundary | *Not directly tested. Requires the hidden `ComputeNeighborhoods: real HDF5 list-chunk oracle` fixture.* | +| 16 | Setup | No feature tuples → return before radius setup | *Not directly tested. No fixture has zero feature tuples.* | +| 17 | Setup | Only background exists → skip averaging and all feature searches | *Not directly tested. No fixture contains only feature 0.* | +| 18 | Setup | Cancel after bin construction → return before searching | *Not directly tested. Requires cancel-signal injection before dispatch.* | +| 19 | Search | Bin has no candidate features → continue | Both analytical cases — sparse centroid layouts | +| 20 | Search | Candidate is outside the feature's radius → exclude it | `ComputeNeighborhoods_MultiplesAnalyticalOracle` — feature 2 excludes feature 1 | +| 21 | Finalize | Write every neighbor list, including empty lists and background | Both analytical cases — count/list-size checks at every index | ## Test inventory +The five ordinary cases use analytical inputs or conversion JSON. All pass in CI. + | Test case | Status | Notes | |-----------|--------|-------| -| `ComputeNeighborhoods_SyntheticOracle` | new-for-V&V | Class 1 microns oracle (7 features, r=3.5, counts `{0,1,2,1,1,0,1}`) + Class 4 count==size and symmetry invariants + inclusive-boundary pin (exact radius-distance pair) + background-feature-0 exclusion assertions. No Equivalent Diameters array (proves microns mode does not require it). | -| `ComputeNeighborhoods_MultiplesAnalyticalOracle` | new-for-V&V | Class 1 per-feature multiples oracle (counts `{0,3,0,0,1,0}`) + count==size invariant + explicit per-feature asymmetry checks + background-feature-0 exclusion assertions (feature 0 coincident with feature 1). | -| `ComputeNeighborhoods_InvalidSearchRadius` | new-for-V&V | 4 SECTIONs: preflight rejects a non-positive search radius (`-5733`), a non-positive multiplier (`-5732`), an eqDiam/centroids tuple mismatch (`-5730`), and Centroids outside an Attribute Matrix (`-5731`) — all error codes asserted. Synthetic input (no archive). | -| `ComputeNeighborhoods_SearchRadiusPreflightInfo` | new-for-V&V | 3 SECTIONs: geometry-info preflight value present; sub-voxel warning `-5734`; oversized warning `-5735`. | -| `ComputeNeighborhoodsFilter: SIMPL Backwards Compatibility` (2 DYNAMIC_SECTIONs) | kept | Validates UUID + argument decoding from SIMPL 6.4/6.5 JSON. | -| `ComputeNeighborhoods_1` | retired | Circular exemplar comparison (golden `Neighborhoods_1` was not an independent oracle) and invalidated by the radius fix. Replaced by the Class 1 analytical oracle + legacy comparison. | -| `ComputeNeighborhoods_3` | retired | Same as `_1` for `mult=3`. | -| `ComputeNeighborhoods_SearchRadiusMicrons` | retired | Premise (microns ≡ multiples when `r = avgDiam/2`) is invalid under the per-feature fix. Microns mode is covered by `SyntheticOracle`. | -| `real HDF5 list-chunk oracle` | new-for-V&V | Actual HDF5 input/output and independent boundary expectations; 79 assertions across the configured options. | - -All non-retired tests pass at the verified commit. *(In-core build confirmed; OOC build to be confirmed per dual-build protocol.)* - -OOC recertification (2026-09-21): serial CTest passed 5/5 in each DREAM3DNX build. `test/ComputeNeighborhoodsTest.cpp::real HDF5 list-chunk oracle` passed 79 assertions in the OOC binary. The hidden case reuses BuildSyntheticFeatures and gives the feature AttributeMatrix shape {3,1,3}. The HDF5 list factory creates three-tuple chunks, so features 2/3 straddle a real list chunk boundary. At positions x=0,3,6, the fixed-radius mode gives lists {3}, {2,4}, {3}; the diameter-based mode gives {3}, {}, {3}. All other features are isolated. Numeric inputs/output are HDF5-OOC; the output list is asserted out-of-core. Existing legacy deviation dispositions remain unchanged. Upstream/develop oracle assertions and tolerances remain intact. No fresh legacy binary comparison is claimed. +| `SimplnxCore::ComputeNeighborhoods_SyntheticOracle` | new-for-V&V | Seven features; radius 3.5; exact counts `{0,1,2,1,1,0,1}`. Checks list sizes, symmetry, inclusive boundary, background exclusion, and operation without Equivalent Diameters. | +| `SimplnxCore::ComputeNeighborhoods_MultiplesAnalyticalOracle` | new-for-V&V | Six features; exact counts `{0,3,0,0,1,0}`. Checks list sizes, asymmetric and symmetric pairs, and background exclusion; covers D4 and D5. | +| `SimplnxCore::ComputeNeighborhoods_InvalidSearchRadius` | new-for-V&V | Four sections assert errors `-5733`, `-5732`, `-5730`, and `-5731` for zero radius, zero multiplier, tuple mismatch, and centroids outside an Attribute Matrix. | +| `SimplnxCore::ComputeNeighborhoods_SearchRadiusPreflightInfo` | new-for-V&V | Three sections check geometry information, no warning for radius 5, warning `-5734` for radius 0.5, and warning `-5735` for radius 1000. | +| `SimplnxCore::ComputeNeighborhoods: real HDF5 list-chunk oracle` | new-for-V&V | Hidden fixture with nine features and tuple shape `{3,1,3}`. Both radius modes assert exact directed lists, counts, storage types, and tuple dimensions across the features 2/3 chunk boundary. | +| `SimplnxCore::ComputeNeighborhoodsFilter: SIMPL Backwards Compatibility` | kept | `SIMPL 6.5 (UUID)` and `SIMPL 6.4 (Filter_Name)` sections check filter identity, empty comments, and six converted argument values. | ## Exemplar archive -- **Archive:** None. All oracle outputs are inline hand-derived values on synthetic inputs. -- The retired tests previously consumed `compute_feature_neighborhoods.tar.gz`; its `download_test_data()` entry was removed from `test/CMakeLists.txt`. -- The legacy comparison (below) used `6_6_stats_test_v2.tar.gz` (shared Small IN100 stats dataset) as **input only**. +- **Archive:** None. All current correctness fixtures use synthetic inputs and inline expected values. +- **SHA512:** Not applicable. `src/Plugins/SimplnxCore/test/CMakeLists.txt` has no archive registration for this filter. +- **Provenance:** Not applicable. Fixture construction and analytical derivations are in `src/Plugins/SimplnxCore/test/ComputeNeighborhoodsTest.cpp`. -## Deviations from DREAM3D 6.5.171 +## Deviations from DREAM.3D v6.5.171 -Comparison run on `6_6_stats_test_v2.dream3d` (Small IN100, 620 features, mult=1) through 6.5.171 `PipelineRunner` and `nxrunner`. +See `vv/deviations/ComputeNeighborhoodsFilter.md` for the source evidence, affected users, and recommendation. Only Part 1 entries are listed here. -- `ComputeNeighborhoodsFilter-D1` — NX counts neighbors with a Euclidean sphere (inclusive `≤` at the boundary); 6.5.171 used an axis-aligned box in normalized bin space with a strict `<`, so NX reports ~52% as many neighbors (correlation 0.894). See `vv/deviations/ComputeNeighborhoodsFilter.md`. -- `ComputeNeighborhoodsFilter-D2` — the `FeaturePhases` required input was removed (unused by both implementations). See `vv/deviations/ComputeNeighborhoodsFilter.md`. -- `ComputeNeighborhoodsFilter-D3` — NX rejects non-positive radius/multiplier values at preflight (`-5732`/`-5733`); 6.5.171 silently accepted them and produced all-zero output. See `vv/deviations/ComputeNeighborhoodsFilter.md`. +| Deviation | Observed difference | +|-----------|---------------------| +| `ComputeNeighborhoodsFilter-D1` | SIMPLNX uses an inclusive Euclidean radius; DREAM.3D v6.5.171 uses strict comparisons in integer bin coordinates. SIMPLNX also offers an absolute-radius mode. | +| `ComputeNeighborhoodsFilter-D2` | SIMPLNX removes the unused Feature Phases input. | +| `ComputeNeighborhoodsFilter-D3` | SIMPLNX rejects non-positive multipliers; DREAM.3D v6.5.171 accepts them. The absolute-radius mode also rejects non-positive radii. | diff --git a/src/Plugins/SimplnxCore/vv/ComputeTriangleGeomCentroidsFilter.md b/src/Plugins/SimplnxCore/vv/ComputeTriangleGeomCentroidsFilter.md index 5bc26f24ce..3f24cfc577 100644 --- a/src/Plugins/SimplnxCore/vv/ComputeTriangleGeomCentroidsFilter.md +++ b/src/Plugins/SimplnxCore/vv/ComputeTriangleGeomCentroidsFilter.md @@ -3,82 +3,106 @@ | | | |--------|--------------| | Plugin | SimplnxCore | -| SIMPLNX UUID | 074c0523-ab7a-4097-b0c3-c4dcbfb9a05e | -| DREAM3D 6.5.171 equivalent | FindTriangleGeomCentroids (DREAM3DReview) — no `FromSIMPLJson` conversion in SimplnxCore | +| SIMPLNX UUID | `074c0523-ab7a-4097-b0c3-c4dcbfb9a05e` | +| SIMPLNX Human Name | Compute Feature Centroids from Triangle Geometry | +| DREAM.3D v6.5.171 equivalent | `FindTriangleGeomCentroids` (SurfaceMeshing plugin) | | Verified commit | ** | -| Status | READY FOR REVIEW | -| Sign-off | *pending second-engineer review* | +| Status | COMPLETE | +| Sign-off | Michael Jackson, 2026-07-13; second engineer: Jared Duffey, 2026-10-06 | ## At a glance -A scannable dashboard for reviewers. Each row is one sentence to one short paragraph — enough that a reader can decide whether they need to read the long-form sections below. - -| Aspect | Current state | -|------------------------|------------------------------------------------------------------------------------------------------------------------------| -| Algorithm Relationship | **Minor changes** — Port of the non-periodic core from legacy `FindTriangleGeomCentroids` (DREAM3DReview); `Is Periodic` is a SIMPLNX-only addition, now reimplemented as a minimum-image mean. | -| Oracle (confirmed) | **Class 1 + Class 4** — hand-built wrapping mesh with closed-form minimum-image centroids; encoded in `ComputeTriangleGeomCentroidsTest.cpp::"Periodic Minimum-Image Oracle"`, passes in-core **and out-of-core**. | -| Code paths enumerated | **8 of 9 exercised**; the feature-AM resize guard (path 9) is left untested (pre-sized AM, low risk). | -| Tests today | **2 test cases** — the kept `12_IN625_GBCD` exemplar (non-periodic) + the new Class 1/4 periodic oracle (`GENERATE` over `IsPeriodic`). | -| Exemplar archive | `12_IN625_GBCD.tar.gz` — non-periodic exemplar inputs+output (kept). Periodic oracle is inline (no archive). | -| Legacy comparison | **Not run yet** — legacy `FindTriangleGeomCentroids` has no periodic mode; non-periodic diff pending Step 8. | -| Bug flags | **2 SIMPLNX bugs found & fixed:** (1) constant half-domain periodic offset (issue #1665) → minimum-image mean; (2) `m_MessageHandler.m_Callback()` direct call → `std::bad_function_call` when periodic branch fired. | -| V&V phase | **All Triangle-filter V&V work complete:** discovery, Class 1/4 oracle, SIMPLNX-vs-oracle reconciliation + 2 bug fixes, algorithm review, dual-build (in-core + OOC) test pass, legacy diff (2 deviations), docs, provenance. **Status:** READY FOR REVIEW, pending second-engineer sign-off at PR. The sibling `ComputeFeatureCentroids` `ImageGeom` overload carried the same constant-offset defect; it was reimplemented as a circular mean and the overload removed in the same branch (see `vv/ComputeFeatureCentroidsFilter.md` Follow-up). | - -For worked instances see `src/Plugins/OrientationAnalysis/vv/BadDataNeighborOrientationCheckFilter.md` and `src/Plugins/OrientationAnalysis/vv/ComputeAvgCAxesFilter.md` (on `topic/vv/compute_avg_caxis`). +| Aspect | Current state | +|--------|---------------| +| Algorithm Relationship | Minor changes. SIMPLNX retains the unique-vertex mean from `FindTriangleGeomCentroids` in DREAM.3D v6.5.171 and adds periodic adjustment and feature-matrix growth. | +| Oracle (confirmed) | Classes 1 and 4: analytical centroids and geometry bounds on one constructed mesh in `test/ComputeTriangleGeomCentroidsTest.cpp`. All pass in CI. | +| Code paths enumerated | 11 of 22 exercised. Gaps include resize, cancellation, invalid matrix selection, Y/Z periodic adjustment, and defensive helper paths. | +| Tests today | 2 test cases: one non-periodic archive comparison and one analytical mesh with periodic mode off and on. All pass in CI. | +| Exemplar archive | `12_IN625_GBCD.tar.gz` supplies a surface mesh and non-periodic reference centroids. The independent oracle is constructed in the test code. | +| Legacy comparison | Not run — comparison artifacts are not available. | +| Bug flags | None | +| V&V phase | Source review, analytical verification, and regressions for both fixed defects are complete. Second-engineer review is complete. | ## Summary -`ComputeTriangleGeomCentroids` computes the centroid of each surface-mesh feature as the mean position of the unique vertices of the triangles carrying that feature's face label, with an optional `Is Periodic` mode for features that wrap the domain boundary. V&V used a **Class 1 (Analytical)** hand-built wrapping mesh (four features: non-wrapping, symmetric wrap, asymmetric wrap, domain-filling) with closed-form minimum-image centroids, plus a **Class 4 (Invariant)** in-domain bound. The periodic path was found to use a **constant half-domain offset** that is only correct for mass symmetric about the seam (issue #1665); it was reimplemented as a largest-empty-gap minimum-image mean. A second latent bug — an unguarded `m_Callback` invocation that threw `std::bad_function_call` when the periodic branch fired — was also fixed. +`ComputeTriangleGeomCentroidsFilter` calculates each feature's centroid from its unique triangle vertices, with optional periodic adjustment. Analytical centroid values and geometry-bound invariants verify both mode settings, and an archive supplies a non-periodic regression comparison. The V&V fixed two periodic defects; all pass in CI. ## Algorithm Relationship -**Minor changes** (Port of the non-periodic core + SIMPLNX-only periodic addition) +*Classification:* Minor changes -*Evidence:* No `FromSIMPLJson` conversion / not in `SimplnxCoreLegacyUUIDMapping`; the non-periodic centroid (arithmetic mean of a feature's unique vertices) mirrors legacy `FindTriangleGeomCentroids` (DREAM3DReview). The `Is Periodic` option and its wrap adjustment are SIMPLNX-only, so there is no legacy periodic behavior to preserve — the periodic path is judged on correctness alone. *(Legacy inspection pending Step 8.)* +*Evidence:* Both implementations average the unique vertices of positive face labels; SIMPLNX adds periodic adjustment and grows the feature Attribute Matrix when a label needs another tuple. ## Oracle -*Class:* **1 (Analytical)** + **4 (Invariant)** +*Class:* 1 (Analytical) and 4 (Invariant). + +*Applied:* One mesh has four features with hand-derived non-periodic and periodic centroids; every non-empty centroid must remain inside the geometry bounds. + +*Encoded:* `src/Plugins/SimplnxCore/test/ComputeTriangleGeomCentroidsTest.cpp::SimplnxCore::ComputeTriangleGeomCentroids: Periodic Minimum-Image Oracle` — one fixture, four populated features and one empty feature, with `IsPeriodic = false` and `IsPeriodic = true`. All pass in CI. -*Applied:* A hand-built triangle mesh on periodic domain X∈[0,4] (all z=0, one distinct y per feature, so only X trips the periodic condition). Expected minimum-image centroids derived by hand via the largest-empty-gap method: F1 non-wrapping (2,1,0); F2 symmetric wrap (0,2,0); F3 asymmetric wrap (3.625,3,0) — **the discriminating case: the old constant offset yields 4.625, outside the domain**; F4 domain-filling → arithmetic-mean fallback (2,1,0). Class 4: every non-empty centroid lies within the geometry bounding box. +*Second-engineer review:* Jared Duffey, 2026-10-06. -*Encoded:* `test/ComputeTriangleGeomCentroidsTest.cpp::"Periodic Minimum-Image Oracle"` — `GENERATE(false, true)` over `IsPeriodic`, 4 features × 3 components + in-bounds invariant. Passes **in-core and out-of-core** (`simplnx-ooc-Rel`). +## Bugs found and fixed -*Second-engineer review:* *pending — the oracle design + hand-derived F3=3.625 value were confirmed with the requesting engineer before encoding; formal second-engineer review at PR.* +All defects in this table are fixed. The fixes will be in the DREAM3D-NX release after v7.4.2. + +| Deviation | Defect | Affected released versions | Resolution | +|-----------|--------|----------------------------|------------| +| `ComputeTriangleGeomCentroidsFilter-D3` | A constant half-domain offset gave incorrect periodic centroids and could put them outside the geometry bounds. | DREAM3D-NX v7.2.1 through v7.4.2. DREAM.3D v6.5.171 was not affected. | The filter calculates a minimum-image mean from vertex coordinates and uses the arithmetic mean when the largest gaps tie. | +| `ComputeTriangleGeomCentroidsFilter-D4` | Periodic adjustment could throw `std::bad_function_call` when no message callback was installed. | DREAM3D-NX v7.2.1 through v7.4.2. DREAM.3D v6.5.171 was not affected. | The filter sends the message through a handler that checks whether a callback exists. | ## Code path coverage -Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeTriangleGeomCentroids.cpp` (~108 lines) + the `AdjustCentroidsForPeriodicFaces` `BoundingBox` overload in `src/simplnx/Utilities/GeometryHelpers.cpp`. Phases: (a) gather per-feature vertex sets from face labels, (b) per-feature arithmetic centroid, (c) optional periodic minimum-image adjustment. +11 of 22 paths exercised. Each uncovered row states the missing input or the guard that prevents entry. + +Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeTriangleGeomCentroids.cpp` (121 lines), `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeTriangleGeomCentroidsFilter.cpp` (131 lines), and `src/simplnx/Utilities/GeometryHelpers.cpp` (351 lines). + +`Oracle` below means `SimplnxCore::ComputeTriangleGeomCentroids: Periodic Minimum-Image Oracle`. `Exemplar` means `SimplnxCore::ComputeTriangleGeomCentroids`. | # | Phase | Path | Test case | |---|-------|------|-----------| -| 1 | (a) gather | `faceLabel > 0` inserts triangle vertices into the feature set | both oracle sections (all features) | -| 2 | (a) gather | `faceLabel == 0/negative` ignored | Oracle — second label 0 on every triangle; feature 0 stays empty | -| 3 | (b) finalize | non-empty feature → arithmetic mean | `IsPeriodic=false` section (F1–F4) | -| 4 | (b) finalize | empty feature set → default (0,0,0) | Oracle — F0 asserted (0,0,0) | -| 5 | (c) periodic | axis spans both faces → minimum-image mean | `IsPeriodic=true` — F2 (unique wrap gap), F3 (unique interior gap) | -| 6 | (c) periodic | axis does not span → component unchanged | `IsPeriodic=true` — F1, and y/z of F2–F4 | -| 7 | (c) periodic | domain-filling (no dominant gap) → arithmetic fallback | `IsPeriodic=true` — F4 | -| 8 | (c) periodic | any axis adjusted → emit info message | Exercised by F2–F4 (regression pin for the `bad_function_call` fix) | -| 9 | resize | feature AM smaller than maxFeatureId+1 → resize | *Not directly tested — the exemplar test's AM is pre-sized; low-risk resize guard.* | +| 1 | Gather | A positive first face label adds the triangle's vertices to its feature set | Oracle — F1 through F4 | +| 2 | Gather | Ignore zero or negative face labels | Oracle — second label 0; Exemplar — first label -1 | +| 3 | Finalize | A non-empty feature gets its arithmetic mean with periodic mode disabled | Oracle — `IsPeriodic = false`, F1 through F4 | +| 4 | Finalize | An empty feature keeps `(0,0,0)`; periodic-face detection returns no faces | Oracle — F0 | +| 5 | Periodic | X touches both opposing faces; a unique largest gap gives the minimum-image mean | Oracle — `IsPeriodic = true`, F2 and F3 | +| 6 | Periodic | An axis does not touch both opposing faces; keep its component | Oracle — F1 and all Y/Z components | +| 7 | Periodic | Multiple largest gaps tie; use the arithmetic mean | Oracle — `IsPeriodic = true`, F4 | +| 8 | Message | Periodic adjustment requests an information message with no installed callback | Oracle — `IsPeriodic = true`, F2 through F4 | +| 9 | Resize | Grow the feature Attribute Matrix to `maxFeatureId + 1` tuples | *Not directly tested. Both fixtures already have enough feature tuples.* | +| 10 | Resize | Return a feature-matrix resize error | *Not directly tested. Requires an injected resize failure.* | +| 11 | Gather | Return when cancellation is requested during triangle traversal | *Not directly tested. Neither test requests cancellation.* | +| 12 | Finalize | Return when cancellation is requested during feature traversal | *Not directly tested. Neither test requests cancellation.* | +| 13 | Preflight | Missing or wrong-type feature Attribute Matrix returns `-12901` | *Not directly tested. Both fixtures select a valid Attribute Matrix.* | +| 14 | Preflight | Valid feature Attribute Matrix creates a float32 centroid array with three components | Oracle; Exemplar | +| 15 | Periodic | Y touches both opposing faces; adjust its centroid component | *Not directly tested. Each oracle feature has a single Y coordinate.* | +| 16 | Periodic | Z touches both opposing faces; adjust its centroid component | *Not directly tested. All oracle vertices have Z = 0.* | +| 17 | Helper | An empty coordinate list returns the domain minimum | *Not directly tested. The filter calls the adjustment only for non-empty feature sets.* | +| 18 | Helper | A non-positive domain length returns the arithmetic mean | *Not directly tested. Periodic-face detection cannot select both faces of a zero-length axis.* | +| 19 | Helper | Normalize a negative coordinate remainder into the domain | *Not directly tested. The geometry minimum bounds every feature coordinate.* | +| 20 | Helper | Normalize a negative final mean remainder into the domain | *Not directly tested. Normalized coordinates and a positive domain length give a non-negative sum.* | +| 21 | Gather | A positive second face label adds the triangle's vertices to its feature set | Exemplar — positive second labels and full centroid-array comparison | +| 22 | Resize | Keep the existing feature-matrix size when it is sufficient | Oracle; Exemplar | ## Test inventory | Test case | Status | Notes | |-----------|--------|-------| -| `SimplnxCore::ComputeTriangleGeomCentroids` (exemplar, `12_IN625_GBCD`) | kept | Non-periodic exemplar comparison; unchanged. | -| `SimplnxCore::ComputeTriangleGeomCentroids: Periodic Minimum-Image Oracle` | new-for-V&V | Class 1 + Class 4; covers periodic + non-periodic; pins both bug fixes. | +| `SimplnxCore::ComputeTriangleGeomCentroids` | kept | Loads `12_IN625_GBCD.tar.gz`, compares the full non-periodic centroid array with the stored array, and checks tuple dimensions. | +| `SimplnxCore::ComputeTriangleGeomCentroids: Periodic Minimum-Image Oracle` | new-for-V&V | `IsPeriodic = false` and `IsPeriodic = true` each check 15 centroid components within absolute tolerance `1e-4`, bounds for four populated features, and tuple dimensions. Covers D3 and D4. | ## Exemplar archive -- **Archive:** `12_IN625_GBCD.tar.gz` (pre-existing; declared in `src/Plugins/OrientationAnalysis/test/CMakeLists.txt`, shared with several surface-mesh tests). Not regenerated by this V&V. +- **Archive:** `12_IN625_GBCD.tar.gz` - **SHA512:** `f696a8af181505947e6fecfdb1a11fda6c762bba5e85fea8d484b1af00bf18643e1d930d48f092ee238d1c19c9ce7c4fb5a8092d17774bda867961a1400e9cea` - **Provenance:** `src/Plugins/SimplnxCore/vv/provenance/12_IN625_GBCD.md` -- **Note:** The archive's `Centroids` reference array is a legacy-derived regression pin (circular oracle) for the non-periodic path only. The authoritative oracle is the inline Class 1 + Class 4 fixture; the periodic branch is not exercised by this archive. -## Deviations from DREAM3D 6.5.171 +## Deviations from DREAM.3D v6.5.171 -Comparison method: source inspection against legacy `FindTriangleGeomCentroids` (SurfaceMeshing plugin). The non-periodic path is a line-for-line match (identical `> 0` label guard, identical float32 arithmetic-mean of set-ordered vertices), so it is bit-identical; two behavioral deviations are documented. +See `vv/deviations/ComputeTriangleGeomCentroidsFilter.md` for the source-based differences in Part 1. -- `ComputeTriangleGeomCentroidsFilter-D1` — `Is Periodic` is a SIMPLNX-only mode; its wrapped centroid (minimum-image mean) has no 6.5.171 analog — see `vv/deviations/ComputeTriangleGeomCentroidsFilter.md` -- `ComputeTriangleGeomCentroidsFilter-D2` — out-of-range face label: 6.5.171 errors (`-99500`), SIMPLNX resizes the Feature Attribute Matrix — see `vv/deviations/ComputeTriangleGeomCentroidsFilter.md` +| Deviation | Observed difference | +|-----------|---------------------| +| `ComputeTriangleGeomCentroidsFilter-D1` | SIMPLNX provides periodic centroid adjustment; DREAM.3D v6.5.171 has no periodic mode. | +| `ComputeTriangleGeomCentroidsFilter-D2` | SIMPLNX grows an undersized feature Attribute Matrix. DREAM.3D v6.5.171 rejects labels above its tuple count and omits the centroid for a label equal to that count. | diff --git a/src/Plugins/SimplnxCore/vv/CopyFeatureArrayToElementArrayFilter.md b/src/Plugins/SimplnxCore/vv/CopyFeatureArrayToElementArrayFilter.md index 1af7d0067c..c7196edba6 100644 --- a/src/Plugins/SimplnxCore/vv/CopyFeatureArrayToElementArrayFilter.md +++ b/src/Plugins/SimplnxCore/vv/CopyFeatureArrayToElementArrayFilter.md @@ -1,134 +1,133 @@ # V&V Report: CopyFeatureArrayToElementArrayFilter -| | | -|-----------|------------------| -| Plugin | SimplnxCore | -| SIMPLNX UUID | `4c8c976a-993d-438b-bd8e-99f71114b9a1` | -| SIMPLNX Human Name | Create Element Array from Feature Array | -| DREAM3D 6.5.171 equivalent | `CopyFeatureArrayToElementArray` (SIMPLib CoreFilters) — SIMPL UUID `99836b75-144b-5126-b261-b411133b5e8a` | -| Verified commit | ** | -| Status | COMPLETE | -| Sign-off | Michael A. Jackson (V&V author). Second engineer: Nathan Young, 2026-07-28 (PR #1689 review). | +| | | +|--------|--------------| +| Plugin | SimplnxCore | +| SIMPLNX UUID | `4c8c976a-993d-438b-bd8e-99f71114b9a1` | +| SIMPLNX Human Name | Create Element Array from Feature Array | +| DREAM.3D v6.5.171 equivalent | `CopyFeatureArrayToElementArray` (SIMPLib CoreFilters, UUID `99836b75-144b-5126-b261-b411133b5e8a`) | +| Verified commit | ** | +| Status | COMPLETE | +| Sign-off | Michael Jackson, 2026-07-24; second engineer: Nathan Young, 2026-07-28 | ## At a glance -| Aspect | Current state | -|------------------------|--------------------------| -| Algorithm Relationship | **Minor changes** — same indirection-copy kernel as SIMPL `CopyFeatureArrayToElementArray`; NX deliberately adds multi-array selection, suffix-based output naming, TBB parallelization, and different feature-count validation semantics. | -| Oracle (confirmed) | **Class 1 (Analytical)** — pure indirection lookup `out[i*C+c] = feature[featureIds[i]*C+c]`; hand-derived expected values on a 4×3×1 fixture (float32/1-comp, int32/3-comp, bool). **Class 4 (Invariant)** companion — piecewise constancy within each feature. Encoded as `CopyFeatureArrayToElementArrayTest.cpp::"Analytical Oracle (Class 1)"`; all pass. | -| Code paths enumerated | 15 of 15 enumerated paths exercised. Direct and Scanline implementations have runtime dispatch witnesses; the real-HDF5 tail test supplements them. | -| Tests today | 24 registered CTest entries plus 1 hidden OOC contract test. The DREAM3DNX OOC build registers the HDF5-OOC manager, and the new 65,536-block tail fixture asserts real input and output stores. | -| Exemplar archive | None — all fixtures are in-memory `AnalyticalFixtures` built in test code; no `download_test_data()` archive required. | -| Legacy comparison | **Run 2026-07-23** (re-run after the kernel fast-path change). Bit-identical numeric output on the main fixture (float32, int32×3, bool). 3 deviations, all naming/validation semantics: D1 (output naming for converted pipelines), D2 (over-provisioned feature array accepted in NX, error -5555 in legacy), D3 (negative ids: silent out-of-bounds garbage in legacy, hard error -5355 in NX). | -| Bug flags | `CopyFeatureArrayToElementArrayFilter-D3` — legacy 6.5.171 silently produces undefined values for negative feature ids (unchecked out-of-bounds read). SIMPLNX behavior is correct. | -| V&V phase | The historical COMPLETE status and sign-off remain. The current DREAM3DNX builds close the previously documented backend gap with real-HDF5 boundary evidence. | +| Aspect | Current state | +|--------|---------------| +| Algorithm Relationship | Minor changes to DREAM.3D v6.5.171 `CopyFeatureArrayToElementArray`. SIMPLNX copies the same feature tuples, with multiple input arrays, suffix-based names and different input checks. | +| Oracle (confirmed) | Classes 1 and 4: literal scalar, RGB and boolean outputs plus equal-feature invariants in `test/CopyFeatureArrayToElementArrayTest.cpp`. All pass in CI. | +| Code paths enumerated | 20 of 28 exercised. Uncovered paths include defensive guards, the Direct fallback, cancellation boundaries and the full-block tail case. | +| Tests today | 16 test definitions: 15 `TEST_CASE` definitions and one template over 10 numeric types. They cover values, validation, bulk I/O failures, cancellation and pipeline conversion; one definition is hidden. | +| Exemplar archive | None. Tests construct inputs and expected values in C++. | +| Legacy comparison | Not run for the current algorithm. The available DREAM.3D v6.5.171 comparison is dated 2026-07-23. | +| Bug flags | None | +| V&V phase | Oracle and regression checks are complete. The current algorithm still requires a legacy comparison. | ## Summary -`CopyFeatureArrayToElementArrayFilter` copies each selected Feature-level array down to the Element (cell) level: for every cell `i`, the output tuple is the source array's tuple at index `featureIds[i]`. Verification used a Class 1 analytical oracle (hand-derived expected values for a 12-cell, 4-feature fixture across single- and multi-component arrays and three data types) plus Class 4 piecewise-constancy invariants; SIMPLNX matched the oracle exactly with zero discrepancies. A/B comparison against DREAM3D 6.5.171 was bit-identical on valid input, with 3 documented deviations in naming/validation semantics — including one legacy bug (silent undefined output for negative feature ids, D3). +`CopyFeatureArrayToElementArrayFilter` copies each selected feature tuple to every cell with that FeatureId. Class 1 literal expectations and Class 4 equal-feature invariants check the copied values. All pass in CI. ## Algorithm Relationship -*Classification:* **Minor changes** +*Classification:* Minor changes -*Evidence:* SIMPL UUID `99836b75-144b-5126-b261-b411133b5e8a` is registered for conversion via `FromSIMPLJson()` (fixtures at `test/simpl_conversion/6_5/` and `6_4/`). The copy kernel is semantically identical to legacy `copyData()` (SIMPL `CopyFeatureArrayToElementArray.cpp:159-191`) — a per-tuple indirection copy. The surrounding filter deliberately changed. - -*Port-time deltas:* - -1. **Multi-array selection** — legacy selects ONE feature array per filter instance; NX takes a `MultiArraySelectionParameter` and loops. Does not change per-array output values. -2. **Output naming** — legacy takes an explicit created-array *name*; NX builds the name as ``. Same numeric output, different output DataPath for converted pipelines (see Deviations D1). -3. **Feature-count validation relaxed** — legacy `execute()` errors (-5555) BOTH when `maxFeatureId >= numFeatures` AND when the feature array is over-provisioned (`maxFeatureId != numFeatures-1`). NX (`ValidateFeatureIdsToFeatureAttributeMatrixIndexing`, `ignoreNegativeValues=false`) errors only when `maxFeatureId >= numFeatures` (-5351); an over-sized feature array is accepted (see Deviations D2). -4. **Negative feature ids** — legacy performs an unchecked negative index into the feature array (undefined behavior / garbage read); NX errors with -5355 (see Deviations D3). -5. **Kernel form** — Direct handles resident arrays; Scanline bulk-loads feature values and copies cells through 65,536-tuple buffers. Both implement the same indirection and perform no floating-point arithmetic. -6. **Type dispatch** — legacy if/else `CanDynamicCast` chain over 11 types (bool + 8 int + 2 float); NX `ExecuteParallelFunction` with `ArrayUseAllTypes` over the same 11 types. No behavioral difference. -7. **Tuple-count precheck (new in NX)** — preflight requires all selected feature arrays to share a tuple count (error -3020). Legacy has no equivalent because it only ever operates on one array. - -*Material PRs since baseline:* #1644 (added -3020 preflight test), #1588 (SIMPL backwards-compat test redesign), #1544 (executeImpl → Algorithm class extraction). None changed the copy kernel. +*Evidence:* `src/Plugins/SimplnxCore/src/SimplnxCore/SimplnxCoreLegacyUUIDMapping.hpp` maps the legacy UUID to this filter; both kernels copy `source[FeatureIds[i] * C + c]`, while SIMPLNX adds multiple-array selection and changes naming and validation. ## Oracle -*Class:* **1 (Analytical)** primary + **4 (Invariant)** companion. Classes 2/3/5 N/A — no external library or paper needed for an indirection lookup (this filter is the literal "Indirection lookups" example in `oracle_classes.md`). +*Class:* 1 (Analytical), with 4 (Invariant). -*Applied:* Hand-built 4×3×1 `ImageGeom` fixture, 12 cells, 4 features. `FeatureIds = [0,1,1,2, 2,0,3,1, 3,3,0,2]`. Feature arrays: `AvgTemp` (float32, 1 comp) `= [10.5, 20.25, -30.75, 40.125]` and `RGB` (int32, 3 comp) `= [(1,2,3), (40,50,60), (-7,8,-9), (100,200,127)]`. Expected cell outputs are derived by hand (spreadsheet-free — 12 lookups) and embedded as inline constants with derivation comments: +*Applied:* A 12-cell, four-feature fixture defines 60 literal scalar, RGB and boolean values; cells with equal FeatureIds must have equal RGB tuples. -- `AvgTemp_Cell = [10.5, 20.25, 20.25, -30.75, -30.75, 10.5, 40.125, 20.25, 40.125, 40.125, 10.5, -30.75]` -- `RGB_Cell` tuple `i` = RGB tuple `FeatureIds[i]`, e.g. cell 3 (fid 2) = `(-7,8,-9)`; cell 6 (fid 3) = `(100,200,127)`. +*Encoded:* `src/Plugins/SimplnxCore/test/CopyFeatureArrayToElementArrayTest.cpp::Analytical Oracle (Class 1)` and `Over-provisioned Feature array accepted` — two fixtures with 60 and 6 literal value checks. All pass in CI. -Class 4 companion invariants: (a) every pair of cells with the same feature id has identical output tuples; (b) each output tuple equals the source feature tuple exactly (bit-identical, no arithmetic performed). - -*Encoded:* `test/CopyFeatureArrayToElementArrayTest.cpp::"Analytical Oracle (Class 1)"` — 12+36+12 element-wise Class 1 assertions (AvgTemp_Cell, RGB_Cell, Active_Cell) with the derivation embedded as comments, plus the Class 4 piecewise-constancy invariant loop. All pass in both builds. A secondary hand-derived fixture lives in `"Over-provisioned Feature array accepted"` (6 assertions). - -*Second-engineer review:* **Signed off by Nathan Young, 2026-07-28** (PR #1689) — covering the oracle design and the test changes. The V&V work was authored by Michael A. Jackson, so the review is independent of the author. +*Second-engineer review:* Nathan Young, 2026-07-28. ## Bugs found and fixed -| Deviation | Defect | Affected released versions | Resolution in this branch | -|-----------|--------|----------------------------|---------------------------| -| `CopyFeatureArrayToElementArrayFilter-D3` | Negative feature identifiers cause unchecked source reads in legacy. | DREAM.3D 6.5.171 only; SIMPLNX rejects these identifiers. | Existing validation returns error -5355. | +All defects in this table are fixed. DREAM3D-NX v7.4.2 contains the fixes for D4, D5 and D6. DREAM3D-NX v7.0.0 already rejects the negative identifiers described by D3. -## Code path coverage +| Deviation | Defect | Affected released versions | Resolution | +|-----------|--------|----------------------------|------------| +| `CopyFeatureArrayToElementArrayFilter-D3` | Negative FeatureIds cause reads outside the source array. | DREAM.3D v6.5.171 was affected. DREAM3D-NX v7.0.0 through v7.4.2 were not affected. | Execution rejects negative FeatureIds with error `-5355` before copying. | +| `CopyFeatureArrayToElementArrayFilter-D4` | Selecting a non-DataArray throws `std::bad_cast` during preflight. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | Parameter validation restricts the selection to DataArray objects. | +| `CopyFeatureArrayToElementArrayFilter-D5` | A slash in the suffix throws `std::invalid_argument`. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | Preflight rejects a slash with error `-3021`. | +| `CopyFeatureArrayToElementArrayFilter-D6` | Empty FeatureIds cause an invalid iterator read during range validation. | DREAM3D-NX v7.2.1 through v7.4.1. DREAM.3D v6.5.171 was not affected. | Empty FeatureIds produce empty output arrays without reading an iterator. | -15 of 15 enumerated paths exercised. Existing failure and cancellation tests are retained; this update adds only the numerical boundary regression. - -Source: `Algorithms/CopyFeatureArrayToElementArray.cpp` (41 lines), `CopyFeatureArrayToElementArrayDirect.cpp` (147 lines), and `CopyFeatureArrayToElementArrayScanline.cpp` (148 lines), plus filter preflight. - -Logical phases: (a) parameter/preflight validation and output creation, (b) execute validation, (c) Direct or Scanline copy. - -| # | Phase | Path | Test case | -|----|---------------|-------------------------------------------------------------------------------------------|-----------| -| 1 | (a) Preflight | empty selection list → error `k_Validate_Empty_Value` (filter's own guard, preflight AND execute) | `Preflight Error - Empty selection (filter guard)` — FeatureIds path is valid so parameter validation passes and the filter guard is what fires | -| 2 | (a) Preflight | non-`IDataArray` selection (NeighborList/StringArray) → rejected by parameter validation. The `ArrayType::DataArray` constraint is what rejects it; `MultiArraySelectionParameter::AllowedDataTypes` is stored but not enforced by the framework | `Preflight Error - Non-DataArray selection rejected` | -| 3 | (a) Preflight | suffix contains `/` → error -3021 | `Preflight Error - Suffix contains '/' (-3021)` | -| 4 | (a) Preflight | selected feature arrays disagree on tuple count → error -3020 | `Preflight Error - Feature array tuple count mismatch (-3020)` | -| 5 | (a) Preflight | valid → one `CreateArrayAction` per selected array (tuple shape from FeatureIds, component shape from source, name ``) | `Analytical Oracle (Class 1)` — 3 arrays created in the Cell AM | -| 6 | (a) Actions | created path collides with an existing object → error -266 at action application (execute in unit tests; pipeline preflight in the GUI); original array untouched | `Execute Error - Created name collides with existing array (-266)` | -| 7 | (b) Execute | negative feature id → error -5355 (`ValidateFeatureIdsToFeatureAttributeMatrixIndexing`, called once — preflight -3020 guarantees equal tuple counts across selected arrays) | `Execute Error - Negative FeatureIds (-5355)` | -| 8 | (b) Execute | `maxFeatureId >= numFeatures` → error -5351 | `Execute Error - FeatureId exceeds Feature tuple count (-5351)` | -| 9 | (b) Execute | over-provisioned feature array (`numFeatures > maxId+1`) → accepted (deviation D2 pin) | `Over-provisioned Feature array accepted` | -| 10 | (b) Execute | zero-tuple FeatureIds → valid no-op (validator's empty guard), empty outputs | `Zero-tuple FeatureIds accepted` | -| 11 | Execution | Pre-cancel and cancel after a bulk read | `Pre-cancelled Scanline does not write`; `Cancellation after FeatureIds chunk read does not write` | -| 12 | (c) Kernel | 11-way type dispatch (bool + 8 int + 2 float) | TEMPLATE_LIST `Valid filter execution` (10 numeric); `Analytical Oracle (Class 1)` (bool, float32, int32) | -| 13 | Direct | In-core type-dispatched component copy | `Analytical Oracle (Class 1)` — explicit in-core dispatch witness and literal scalar/RGB/bool outputs | -| 14 | Scanline | Feature cache plus 65,536-cell I/O blocks and partial tail | `Analytical Oracle (Class 1)` — OOC dispatch witness; `genuine HDF5 65536-block tail oracle` — exact 3-component values on disk | -| 15 | Bulk I/O | Read/write failures propagate before partial writes | `Scanline propagates bulk failures before partial writes` | +## Code path coverage +20 of 28 paths exercised. Each uncovered path has a reason below. + +Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/CopyFeatureArrayToElementArray.cpp` (41 lines), `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/CopyFeatureArrayToElementArrayDirect.cpp` (147 lines), `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/CopyFeatureArrayToElementArrayScanline.cpp` (148 lines), `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/CopyFeatureArrayToElementArrayFilter.cpp` (164 lines), and `src/simplnx/Utilities/DataArrayUtilities.cpp` (515 lines). + +Test names below omit the common `SimplnxCore::CopyFeatureArrayToElementArrayFilter: ` prefix. Direct and Scanline value fixtures use in-memory stores. + +| # | Phase | Path | Test case | +|---|-------|------|-----------| +| 1 | Preflight | Reject an empty selection in preflight and execution | `Preflight Error - Empty selection (filter guard)` | +| 2 | Parameters | Reject non-DataArray selections | `Preflight Error - Non-DataArray selection rejected` — NeighborList input | +| 3 | Preflight | Reject a slash in the suffix with `-3021` | `Preflight Error - Suffix contains '/' (-3021)` | +| 4 | Preflight | Reject different feature-array tuple counts with `-3020` | `Preflight Error - Feature array tuple count mismatch (-3020)` | +| 5 | Preflight | Create one output per source, with cell tuple shape and source component shape | `Analytical Oracle (Class 1)` | +| 6 | Actions | Reject an existing output path with `-266`; preserve the original array | `Execute Error - Created name collides with existing array (-266)` | +| 7 | Validation | Reject negative FeatureIds with `-5355`; leave outputs at zero | `Execute Error - Negative FeatureIds (-5355)` | +| 8 | Validation | Reject FeatureIds at or above the feature tuple count with `-5351` | `Execute Error - FeatureId exceeds Feature tuple count (-5351)` | +| 9 | Validation | Accept unused feature tuples | `Over-provisioned Feature array accepted` | +| 10 | Validation | Accept empty FeatureIds and create empty outputs | `Zero-tuple FeatureIds accepted` | +| 11 | Dispatch | Return early for an empty algorithm selection | *Not directly tested. Filter validation rejects the empty selection before these defensive guards.* | +| 12 | Dispatch | Copy all 11 scalar types | `Valid filter execution` — 10 numeric types; `Analytical Oracle (Class 1)` — boolean values | +| 13 | Direct | Copy components through raw pointers | `Analytical Oracle (Class 1)`; `Valid filter execution` | +| 14 | Direct | Copy through the generic store fallback | *Not directly tested. The fixtures use concrete DataStore objects.* | +| 15 | Direct | Cancel before copying a selected array | *Not directly tested. The cancellation fixtures invoke Scanline only.* | +| 16 | Direct | Cancel inside the raw-pointer cell loop | *Not directly tested. Requires cancellation during the Direct copy.* | +| 17 | Direct | Cancel inside the generic store loop | *Not directly tested. Requires a fallback store and cancellation during the copy.* | +| 18 | Scanline | Cache feature values and copy a partial cell block | `Analytical Oracle (Class 1)` — literal scalar, RGB and boolean outputs | +| 19 | Scanline | Copy a full 65,536-cell block and a partial tail | *Not directly tested. The matching `genuine HDF5 65536-block tail oracle` fixture is hidden and requires an HDF5 store manager.* | +| 20 | Validation | Propagate a FeatureIds read failure | `Scanline propagates bulk failures before partial writes` / `validator FeatureIds read fails` | +| 21 | Scanline | Propagate a feature-cache read failure | `Scanline propagates bulk failures before partial writes` / `feature cache read fails` | +| 22 | Scanline | Propagate a cell-block read failure | `Scanline propagates bulk failures before partial writes` / `FeatureIds transfer read fails after validation` | +| 23 | Scanline | Propagate an output write failure | `Scanline propagates bulk failures before partial writes` / `output write fails` | +| 24 | Scanline | Honor cancellation before validation and array copying | `Pre-cancelled Scanline does not write` | +| 25 | Scanline | Honor cancellation after reading a cell block, before copying its values | `Cancellation after FeatureIds chunk read does not write` | +| 26 | Scanline | Cancel at the start of a later cell block | *Not directly tested. Requires cancellation between two block transfers.* | +| 27 | Scanline | Cancel after filling the output buffer, before writing it | *Not directly tested. Requires cancellation at this write boundary.* | +| 28 | Conversion | Convert the selected path and FeatureIds path; leave the suffix empty | `SIMPL Backwards Compatibility` — both conversion fixtures | ## Test inventory +Test names omit the common `SimplnxCore::CopyFeatureArrayToElementArrayFilter: ` prefix. + | Test case | Status | Notes | |-----------|--------|-------| -| `Preflight Error - Empty selection (filter guard)` | new-for-V&V | Supplies a valid FeatureIds array so the filter's own empty-selection guard is what fires, in both preflight and execute. Replaces `Parameter Check` (see the retired row below). | -| `Preflight Error - Feature array tuple count mismatch (-3020)` | kept | Two feature arrays with 3 vs 4 tuples → -3020. | -| `Preflight Error - Non-DataArray selection rejected` | new-for-V&V | Pins the `ArrayType::DataArray` parameter constraint: selecting a `NeighborList` previously threw an uncaught `std::bad_cast` out of preflight. | -| `Preflight Error - Suffix contains '/' (-3021)` | new-for-V&V | Pins the -3021 suffix validation: a suffix containing `/` previously threw an uncaught `std::invalid_argument` from `DataObject` name validation. | -| `Execute Error - Created name collides with existing array (-266)` | new-for-V&V | Empty suffix + self-selection → -266 when the output actions are applied (execute in unit tests, pipeline preflight in the GUI, since `IFilter::preflight` does not apply actions); asserts the source array is untouched (no silent overwrite). | -| `Analytical Oracle (Class 1)` | new-for-V&V | ImageGeom fixture; 60 element-wise Class 1 assertions vs hand-derived constants (float32/1, int32/3, bool/1) + Class 4 piecewise-constancy loop + `CheckArraysInheritTupleDims`. | -| `Execute Error - Negative FeatureIds (-5355)` | new-for-V&V | Passes preflight, fails execute; pins deviation D3's SIMPLNX side. | -| `Execute Error - FeatureId exceeds Feature tuple count (-5351)` | new-for-V&V | id 4 vs 4-tuple feature array. | -| `Over-provisioned Feature array accepted` | new-for-V&V | 8-tuple feature array, max id 2 → success + 6 hand-derived value assertions; pins deviation D2. | -| `Zero-tuple FeatureIds accepted` | new-for-V&V | Degenerate no-op case; also pins the empty-store guard in `ValidateFeatureIdsToFeatureAttributeMatrixIndexing`, which previously dereferenced `end()` on a zero-tuple FeatureIds array (UB). | -| `Valid filter execution` (TEMPLATE_LIST ×10 types) | kept (modified) | Was comparing never-initialized feature data (indeterminate values) and all-zero temperature data — an indexing bug could not have been detected. Now initialized with distinct per-feature values `[5,15,25]` / `[1,4,7]`. Added `REQUIRE_NOTHROW`, `CAPTURE(i)`, `CheckArraysInheritTupleDims`. | -| `SIMPL Backwards Compatibility` (2 DYNAMIC_SECTIONs) | kept (modified) | UUID + argument conversion round-trip for 6.4 and 6.5 fixtures; now also asserts the converted multi-path selection value (was previously unasserted). | -| `Parameter Check` | retired | Its empty-selection assertion supplied an empty FeatureIds path, which failed *parameter* validation before `preflightImpl()` ran, so the filter's own empty-selection guard was never reached. Replaced by `Preflight Error - Empty selection (filter guard)`. | -| `Scanline propagates bulk failures before partial writes` | kept | Existing read/write failure regressions. | -| `Pre-cancelled Scanline does not write` | kept | Existing cancellation regression. | -| `Cancellation after FeatureIds chunk read does not write` | kept | Existing cancellation regression after I/O. | -| `genuine HDF5 65536-block tail oracle` | new-for-V&V | 65,537 cell identifiers and int32 RGB tuples. Checks literal outputs at cell 0, cell 65,535, and the one-cell tail. | - - -The upstream/develop independent expected values and tolerances are preserved. The registered oracle runs both Direct and Scanline implementations through runtime witnesses. The hidden test requires HDF5-OOC and checks exact output tuples [1,2,3], [40,50,60], and [-7,8,-9]. Existing DREAM3D 6.5.171 deviations D1–D3 remain unchanged. - -OOC recertification, 2026-09-18: serial CTest passed 24/24 in `NX-Com-Qt69-Vtk96-Rel` and 24/24 in `NX-Com-Qt69-Vtk96-OoC-Rel`. The new hidden boundary case passed 22 assertions in the OOC binary and is included in the OOC-only `SimplnxCoreOocStoreContracts` CTest entry. The original report status and sign-off above are historical and unchanged. +| `Preflight Error - Empty selection (filter guard)` | new-for-V&V | Uses a valid FeatureIds path and checks the filter's empty-selection error in preflight and execution. | +| `Preflight Error - Feature array tuple count mismatch (-3020)` | kept | Selects arrays with three and four tuples; checks `-3020`. | +| `Preflight Error - Non-DataArray selection rejected` | new-for-V&V | Selects a NeighborList and checks invalid preflight. Covers D4. | +| `Preflight Error - Suffix contains '/' (-3021)` | new-for-V&V | Checks `-3021` for a slash in the suffix. Covers D5. | +| `Execute Error - Created name collides with existing array (-266)` | new-for-V&V | Checks the action error and every original FeatureIds value. | +| `Analytical Oracle (Class 1)` | new-for-V&V | Checks 60 literal scalar, RGB and boolean values, equal-feature RGB invariants and inherited tuple dimensions. | +| `genuine HDF5 65536-block tail oracle` | new-for-V&V | Hidden (`[.OocStoreContract]`). Requires HDF5 input and output stores; checks literal RGB tuples at cells 0, 65,535 and 65,536. | +| `Scanline propagates bulk failures before partial writes` | kept | Injects four read/write errors; checks error codes and unchanged output sentinels for the read failures. | +| `Pre-cancelled Scanline does not write` | kept | Sets cancellation before execution and checks all output sentinels. | +| `Cancellation after FeatureIds chunk read does not write` | kept | Cancels after the second FeatureIds read and checks all output sentinels. | +| `Execute Error - Negative FeatureIds (-5355)` | new-for-V&V | Checks `-5355` after successful preflight and verifies all three output arrays remain zero. Covers D3. | +| `Execute Error - FeatureId exceeds Feature tuple count (-5351)` | new-for-V&V | Uses ID 4 with four feature tuples; checks `-5351` and zero output arrays. | +| `Over-provisioned Feature array accepted` | new-for-V&V | Uses eight feature tuples with maximum ID 2; checks six literal outputs. Covers D2. | +| `Zero-tuple FeatureIds accepted` | new-for-V&V | Checks successful execution and zero output tuples. Covers D6. | +| `Valid filter execution` | kept | Template over 10 numeric types; checks two 30-cell outputs and tuple dimensions. V&V replaced uninitialized or constant inputs with distinct feature values. | +| `SIMPL Backwards Compatibility` | kept | `SIMPL 6.5 (UUID)` and `SIMPL 6.4 (Filter_Name)` sections check UUID and arguments, including the selected path and empty suffix. | ## Exemplar archive -- **Archive:** None. All fixtures are built in-memory in the test file (`AnalyticalFixtures`); no exemplar `.dream3d`/`download_test_data()` entry exists or is needed for this filter. -- **Provenance:** N/A (no archive). The A/B comparison inputs are minted by `make_input.py` in the comparison bundle (OneDrive archive), reproducible from the checked-in oracle constants. +- **Archive:** None. Fixtures are constructed in `src/Plugins/SimplnxCore/test/CopyFeatureArrayToElementArrayTest.cpp`. +- **SHA512:** Not applicable. `src/Plugins/SimplnxCore/test/CMakeLists.txt` registers no archive for this filter. +- **Provenance:** Not applicable; no exemplar archive. Comparison evidence is cited in `src/Plugins/SimplnxCore/vv/deviations/CopyFeatureArrayToElementArrayFilter.md`. -## Deviations from DREAM3D 6.5.171 +## Deviations from DREAM.3D v6.5.171 -Comparison run 2026-07-23 on the analytical fixture (main case) plus two targeted probes. **Numeric output bit-identical on valid input** (float32/1-comp, int32/3-comp, bool arrays). +See `vv/deviations/CopyFeatureArrayToElementArrayFilter.md` for root causes, affected users and recommendations. -- `CopyFeatureArrayToElementArrayFilter-D1` — converted legacy pipelines produce differently *named* output arrays (`` vs explicit legacy name) — see `vv/deviations/CopyFeatureArrayToElementArrayFilter.md` -- `CopyFeatureArrayToElementArrayFilter-D2` — over-provisioned feature array errors in 6.5.171 (-5555) but succeeds in SIMPLNX — see `vv/deviations/CopyFeatureArrayToElementArrayFilter.md` -- `CopyFeatureArrayToElementArrayFilter-D3` — **legacy bug**: negative feature ids silently produce undefined values in 6.5.171 (unchecked out-of-bounds read); SIMPLNX errors -5355 — see `vv/deviations/CopyFeatureArrayToElementArrayFilter.md` +| Deviation | Observed difference | +|-----------|---------------------| +| `CopyFeatureArrayToElementArrayFilter-D1` | Converted pipelines retain the source array name in SIMPLNX; DREAM.3D uses the explicit output name. | +| `CopyFeatureArrayToElementArrayFilter-D2` | SIMPLNX accepts unused feature tuples; DREAM.3D requires the feature tuple count to equal the largest FeatureId plus one. | +| `CopyFeatureArrayToElementArrayFilter-D3` | SIMPLNX rejects negative FeatureIds; DREAM.3D can read outside the source array. | diff --git a/src/Plugins/SimplnxCore/vv/CreateFeatureArrayFromElementArrayFilter.md b/src/Plugins/SimplnxCore/vv/CreateFeatureArrayFromElementArrayFilter.md index 5a286aa332..b29e937327 100644 --- a/src/Plugins/SimplnxCore/vv/CreateFeatureArrayFromElementArrayFilter.md +++ b/src/Plugins/SimplnxCore/vv/CreateFeatureArrayFromElementArrayFilter.md @@ -1,161 +1,127 @@ # V&V Report: CreateFeatureArrayFromElementArrayFilter -| | | -|-----------|------------------| -| Plugin | SimplnxCore | -| SIMPLNX Human Name | Create Feature Array from Element Array | +| | | +|--------|--------------| +| Plugin | SimplnxCore | | SIMPLNX UUID | `50e1be47-b027-4f40-8f70-1283682ee3e7` | -| DREAM3D 6.5.171 equivalent | `CreateFeatureArrayFromElementArray` (SIMPL UUID `94438019-21bb-5b61-a7c3-66974b9a34dc`) | +| SIMPLNX Human Name | Create Feature Array from Element Array | +| DREAM.3D v6.5.171 equivalent | `CreateFeatureArrayFromElementArray` (SIMPL UUID `94438019-21bb-5b61-a7c3-66974b9a34dc`) | | Verified commit | ** | | Status | COMPLETE | -| Sign-off | *Nathan Young, 07-28-2026* — second engineer: *Michael Jackson <mike.jackson@bluequartz.net>, 08-07-2026* | +| Sign-off | Nathan Young, 2026-07-28; second engineer: Michael Jackson, 2026-08-07 | ## At a glance -| Aspect | Current state | -|------------------------|--------------------------| -| Algorithm Relationship | **Port** — the per-cell copy loop is a line-by-line translation of SIMPL `CreateFeatureArrayFromElementArray`. UUID changed from SIMPL; legacy alias maintained via `FromSIMPLJson()` and SIMPL conversion fixtures. The sizing logic differs only in the AM under-sized case: SIMPL errors; SIMPLNX resizes and succeeds (see D1 in deviations). All pipelines that succeeded in SIMPL produce bit-identical output in SIMPLNX. | -| Oracle | **Class 1 (Analytical) primary + Class 4 (Invariant) companion** — expected outputs hand-derived for 9-fixture AnalyticalFixtures suite (AF-1: single-component consistent, AF-2: single-component inconsistent/warning, AF-3: three-component consistent, AF-4: error -81880 all-negative featureIds, AF-5: error -81881 shrink-protection guard, AF-6: gap in FeatureIds range resize-grown tuple, AF-7: error -81882 empty featureIds, AF-8: error -81880 mixed-negative featureIds, AF-9: error -81883 preflight tuple count mismatch). Class 4 invariants: output array has `max(featureIds)+1` tuples; output data type and component shape match input; error codes for boundary inputs. Implemented as inline `REQUIRE` assertions in `test/CreateFeatureArrayFromElementArrayTest.cpp`. | -| Code paths enumerated | **10 of 11 paths exercised; 1 uncovered:** cancel check. Error paths -81880 and -81881 covered by AF-4/AF-8 and AF-5; -81882 by AF-7; -81883 (preflight) by AF-9. | -| Tests today | **12 test cases** (2 regression + 1 SIMPL backwards-compat + 9 AnalyticalFixtures). **Circular-oracle flag**: existing regression tests compare SIMPLNX output against legacy-generated `CellFeatureData` arrays pre-existing in `6_5_test_data_1_v2.dream3d`; not a valid correctness oracle per policy. AF-1 through AF-3 provide the independent Class 1 + Class 4 output oracle; AF-4 through AF-9 cover error-path and boundary-condition Class 4 invariants. | -| Exemplar archive | `6_5_test_data_1_v2.tar.gz` (SHA512 `585b51b…`) — shared input archive. Pre-existing `CellFeatureData` arrays within the dream3d are legacy-generated; serve only as regression baselines. No oracle-specific archive needed for Class 1 (oracle is inline assertions). | -| Legacy comparison | **Complete (2026-07-23) — one deviation identified, no migration impact.** Empirical A/B comparison on synthetic 8×1×1 fixtures: bit-identical. Static source analysis identified D1 (AM under-sized: SIMPL errors -5555; SIMPLNX resizes and succeeds). D1 is unreachable from any SIMPL pipeline that ran successfully, so output is bit-identical for the entire valid SIMPL migration space. See `vv/deviations/CreateFeatureArrayFromElementArrayFilter.md`. | -| Bug flags | **Three defects resolved:** (1) UB on empty featureIds: `std::minmax_element` on empty range is UB; fixed by empty-array guard → error -81882. (2) UB on negative featureIds: old `maxValue < 0` guard missed mixed-sign inputs; negative featureIdx in copy loop converts to `usize(UINT64_MAX)` → OOB write; fixed by `minValue < 0` guard → error -81880. (3) Unchecked tuple-count precondition: cell array and featureIds tuple counts were never compared; OOB featureIds access when counts differ; fixed by preflight check → error -81883. | -| V&V phase | **Complete.** Oracle designed (Class 1 + Class 4) and implemented; code-path analysis complete; Algorithm Relationship classified (Port); AnalyticalFixtures tests implemented (AF-1 through AF-9); legacy A/B comparison run (2026-07-23, bit-identical on full valid SIMPL migration space); one deviation (D1, SIMPLNX improvement) identified and documented. | +| Aspect | Current state | +|--------|---------------| +| Algorithm Relationship | Minor changes to `CreateFeatureArrayFromElementArray` in DREAM.3D v6.5.171. Chunked reads and a final output write preserve the last-value rule; validation and feature-matrix sizing differ. | +| Oracle (confirmed) | Classes 1 and 4: hand-derived values, shape checks and error expectations in 10 constructed fixtures in `test/CreateFeatureArrayFromElementArrayTest.cpp`. All pass in CI. | +| Code paths enumerated | 15 of 25 exercised. Cancellation, transfer failures, resize failure, the no-growth case and remaining data types lack direct tests. | +| Tests today | 13 test cases: 2 archive regressions, 9 AF fixtures, 1 bulk-boundary oracle and 1 SIMPL conversion case. All pass in CI. | +| Exemplar archive | `6_5_test_data_1_v2.tar.gz` supplies Small IN100 inputs and two stored regression arrays. Independent expected values are in C++ fixtures. | +| Legacy comparison | Not run on the current algorithm. The previous DREAM.3D v6.5.171 comparison ran on 2026-07-23. | +| Bug flags | None | +| V&V phase | Oracle fixtures, source analysis and defect records are complete. The legacy comparison predates the current algorithm. | ## Summary -`CreateFeatureArrayFromElementArrayFilter` copies each element-level data array value to the feature-level entry identified by the corresponding FeatureId, using last-writer-wins semantics, and emits one warning if any cell's value for a feature differs from the first-seen value. The filter was verified analytically using a Class 1 hand-derived oracle on three small synthetic fixtures covering consistent, inconsistent, and multi-component cases (AF-1, AF-2, AF-3), plus Class 4 invariants on output shape and type, and six error-path and boundary-condition fixtures: the all-negative-featureIds guard (AF-4, error -81880), the shrink-protection guard (AF-5, error -81881), a gap in FeatureIds range with a resize-grown never-written tuple (AF-6), the empty-featureIds guard (AF-7, error -81882), the mixed-negative-featureIds guard (AF-8, error -81880), and the preflight tuple-count-mismatch guard (AF-9, error -81883). All oracle assertions pass. The per-cell copy loop is a direct port of SIMPL `CreateFeatureArrayFromElementArray`; empirical A/B comparison (2026-07-23) confirmed bit-identical output for all inputs that SIMPL can process. One deviation exists (D1): SIMPLNX handles the AM under-sized case by resizing, whereas SIMPL errors with -5555. This deviation has no migration impact — it is unreachable from any pipeline that ran successfully in SIMPL. +`CreateFeatureArrayFromElementArrayFilter` copies element values into feature tuples and keeps the last value for each feature. Hand-derived fixtures check output values, one-warning behavior, array shapes and input errors; all pass in CI. Four fixed defects cover negative IDs, unequal input tuple counts, empty IDs and loss of existing feature data during resizing. ## Algorithm Relationship -*Classification:* **Port** - -*Evidence:* The SIMPLNX algorithm at `Algorithms/CreateFeatureArrayFromElementArray.cpp` (121 lines) is a line-for-line translation of the SIMPL `CreateFeatureArrayFromElementArray` filter. Identical control flow: iterate over cell tuples, look up `featureIds[cellIdx]`, record first-seen tuple offset in `featureFirstCellOffset`, compare current value against first-seen per component, emit one warning on first mismatch, write current value to `createdDataStore[featureId * C + comp]` (last-writer-wins). The SIMPL UUID (`94438019-21bb-5b61-a7c3-66974b9a34dc`) differs from the SIMPLNX UUID (`50e1be47-b027-4f40-8f70-1283682ee3e7`) because the filter was re-UUID-ed during the SIMPL→SIMPLNX port; the legacy alias is maintained through `FromSIMPLJson()` and `test/simpl_conversion/6_5/CreateFeatureArrayFromElementArrayFilter.json`. - -*Port-time deltas that do not change output data:* - -1. `QMap` → `std::vector` for `featureFirstCellOffset` — SIMPL used an O(log n) sorted map from feature ID to raw source pointer; SIMPLNX uses an O(1) flat vector pre-allocated to `maxValue + 1` elements (initialized to `k_NotSeen = std::numeric_limits::max()`), indexed directly by feature ID, storing the first-seen flat element offset. Lookup semantics and iteration order over cells are unchanged. -2. SIMPL `EXECUTE_FUNCTION_TEMPLATE` macro → `ExecuteDataFunction(CopyCellDataFunctor{}, ...)` dispatch — API modernization; identical runtime dispatch on element data type. -3. Warning API: SIMPL used `filter->setWarningCondition(-1000, ss)` guarded by `bool warningThrown = false;`; SIMPLNX uses `result.warnings().push_back(Warning{-1000, ...})` guarded by `result.warnings().empty()`. Both guards produce the same behavior: exactly **one** warning per execution when any feature's cell values are inconsistent. Confirmed by empirical A/B comparison (2026-07-23) — no behavioral delta. -4. Algorithm class extracted from `executeImpl` into `Algorithms/CreateFeatureArrayFromElementArray.{hpp,cpp}` — code organization change only (PRs #1301 and #1544). -5. Added `shouldCancel` check inside the per-cell loop — no output change (cancel path was absent in SIMPL). +*Classification:* Minor changes -*Material PRs:* - -- **#1301** — "ENH: Move Execution to Algorithm Classes": extracted algorithm class skeleton. -- **#1544** — "ENH: Move non-trivial Filter executeImpl() logic to Algorithm classes": finalized algorithm class with resize logic. -- **#1278** — "BUG: Ensure FeatureId arrays are range checked against the Feature Attribute Matrix": Updated help text and default value for parameter. -- **#1295** — "ENH: Add Fill Functionality to CreateArrayAction": output array initialized to `"0"` fill at creation; ensures feature-0 slot has a defined value when no cell maps to feature 0. +*Evidence:* Both implementations compare each tuple with its feature's first tuple and retain its last tuple; SIMPLNX adds input guards, grows undersized feature matrices and uses 65,536-tuple input chunks. ## Oracle -*Class:* **1 (Analytical)** primary, **4 (Invariant)** companion. - -*Applied (Class 1 — Analytical):* Expected outputs are hand-derived without reference to any DREAM3D implementation. The algorithm is a pure indirection: for each cell `i` in order, write `output[featureIds[i] * C + j] = cellInput[i * C + j]` for each component `j`. Last-writer-wins when multiple cells share a featureId. The output AM is resized to `max(featureIds[:]) + 1` tuples. - -**Fixture AF-1** — single-component, all values consistent: - -| | | -|---|---| -| `featureIds` | `[0, 1, 2, 1, 2]` | -| `cellValues` (float32, 1-comp) | `[5.0, 10.0, 20.0, 10.0, 20.0]` | +*Class:* 1 (Analytical), with 4 (Invariant) checks. -Hand derivation: -- Feature 0: only cell 0 → `output[0] = 5.0` -- Feature 1: cell 1 (first) = `10.0`; cell 3 (second) = `10.0` (same → no warning); last-writer = `10.0` -- Feature 2: cell 2 (first) = `20.0`; cell 4 (second) = `20.0` (same → no warning); last-writer = `20.0` +*Applied:* For each feature and component, select the last input value with that FeatureId; expect zero for unused features, at most one inconsistency warning and the specified input errors. -Expected output (float32, 3 tuples): `[5.0, 10.0, 20.0]`. Expected warnings: **0**. +*Encoded:* `src/Plugins/SimplnxCore/test/CreateFeatureArrayFromElementArrayTest.cpp::SimplnxCore::CreateFeatureArrayFromElementArrayFilter: AF-1 single-component consistent` through `AF-9 error path featureIds tuple count mismatch`, plus `Bulk last-value boundary oracle` — 10 fixtures. All pass in CI. -**Fixture AF-2** — single-component, inconsistent values (warning path): +*Second-engineer review:* Michael Jackson, 2026-08-07, for AF-1 through AF-9. -| | | -|---|---| -| `featureIds` | `[1, 2, 1, 2]` | -| `cellValues` (float32, 1-comp) | `[10.0, 20.0, 15.0, 20.0]` | +## Bugs found and fixed -Hand derivation: -- Feature 0: never written; default fill `"0"` → `output[0] = 0.0` -- Feature 1: cell 0 (first) = `10.0` written; cell 2 (second) = `15.0` ≠ `10.0` → **one warning emitted** (Warning -1000: "Elements from Feature 1 do not all have the same value…"); `output[1] = 15.0` (last-writer) -- Feature 2: cell 1 (first) = `20.0`; cell 3 (second) = `20.0` (same → no additional warning); `output[2] = 20.0` +All defects in this table are fixed. DREAM3D-NX v7.4.2 contains all four fixes. -Expected output (float32, 3 tuples): `[0.0, 15.0, 20.0]`. Expected warnings: **1** (Warning -1000 for feature 1). - -**Fixture AF-3** — 3-component, consistent: - -| | | -|---|---| -| `featureIds` | `[1, 2, 1, 2]` | -| `cellValues` (uint8, 3-comp) | `cell0=[10,20,30]`, `cell1=[40,50,60]`, `cell2=[10,20,30]`, `cell3=[40,50,60]` | - -Hand derivation: -- Feature 0: never written → `output[0] = [0, 0, 0]` -- Feature 1: cell 0 `[10,20,30]`; cell 2 `[10,20,30]` (same → no warning); `output[1] = [10, 20, 30]` -- Feature 2: cell 1 `[40,50,60]`; cell 3 `[40,50,60]` (same → no warning); `output[2] = [40, 50, 60]` - -Expected output (uint8, 3 tuples): `[[0,0,0], [10,20,30], [40,50,60]]`. Expected warnings: **0**. - -*Applied (Class 4 — Invariant):* Derivable properties any valid output must satisfy, asserted inline: - -- `outputArray.getNumberOfTuples() == max(featureIds[:]) + 1` (output array resized consistently with AM) -- `outputArray.getDataType() == inputCellArray.getDataType()` -- `outputArray.getNumberOfComponents() == inputCellArray.getNumberOfComponents()` -- `result.warnings().size() == 0` when all cells for each feature are value-consistent; `result.warnings().size() == 1` when any mismatch exists (one-warning-only guard) -- execute returns error -81882 when the featureIds array is empty; execute returns error -81880 when any feature ID is negative (`minValue < 0`; covers all-negative and mixed cases); execute returns error -81881 when an AM child array has more tuples than the resize target; preflight returns error -81883 when cell array and featureIds tuple counts differ - -*Encoded:* Implemented — `AnalyticalFixtures` TEST_CASEs in `test/CreateFeatureArrayFromElementArrayTest.cpp` (AF-1 through AF-9). `getNumberOfTuples`, `getDataType`, `getNumberOfComponents` asserted in the Validation block of AF-1/AF-2/AF-3/AF-6; `warnings().size()` asserted in the Execution block immediately after `filter.execute()`; execute error code invariants asserted in AF-4, AF-5, AF-7, and AF-8; preflight error code invariant asserted in AF-9. - -*Second-engineer review:* **Michael Jackson <mike.jackson@bluequartz.net>, 08-07-2026** — reviewed at PR #1695. Scope: the Class 1 hand derivations for AF-1/AF-2/AF-3/AF-6 against the closed form `output[featureId * C + comp] = input[cellIdx * C + comp]` (last-writer) at lines 25–56 of `Algorithms/CreateFeatureArrayFromElementArray.cpp`; the Class 4 invariant set; and the error-code mapping for AF-4/AF-5/AF-7/AF-8/AF-9 against the guards in the algorithm (-81880 `minValue < 0`, -81881 shrink protection, -81882 empty featureIds) and in `preflightImpl` (-81883 tuple-count mismatch). No oracle-design defects found; the derivations are independent of the SIMPLNX implementation and the fixture set covers the consistent, inconsistent, multi-component, resize-gap, and every error boundary. +| Deviation | Defect | Affected released versions | Resolution | +|-----------|--------|----------------------------|------------| +| `CreateFeatureArrayFromElementArrayFilter-D2` | Negative FeatureIds could index outside the output array. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | Rejects any negative ID with error `-81880` before copying. | +| `CreateFeatureArrayFromElementArrayFilter-D3` | Unequal input tuple counts could read beyond FeatureIds. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | Preflight rejects unequal counts with error `-81883`. | +| `CreateFeatureArrayFromElementArrayFilter-D4` | An empty FeatureIds array caused an out-of-bounds read when finding its maximum. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | Rejects empty FeatureIds with error `-81882`. | +| `CreateFeatureArrayFromElementArrayFilter-D5` | Unconditional resizing could truncate existing feature arrays. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | Preserves larger matrices and rejects growth that would truncate a child array with error `-81881`. | ## Code path coverage -*10 of 11 paths exercised; 1 uncovered (cancel check).* +15 of 25 paths exercised. The uncovered rows identify the remaining gaps. -Sources: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/CreateFeatureArrayFromElementArray.cpp` (121 lines); `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/CreateFeatureArrayFromElementArrayFilter.cpp` (preflightImpl, path #11) +Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/CreateFeatureArrayFromElementArray.cpp` (214 lines); `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/CreateFeatureArrayFromElementArrayFilter.cpp` (149 lines). -The algorithm has two phases: **(a) `operator()()` sizing and guard logic** — guard empty featureIds; scan `featureIds` for `minValue` and `maxValue` via `std::minmax_element`; error when `minValue < 0`; when `maxValue + 1 > AM.tupleCount`, enter the grow block — inner shrink-protection loop iterates all AM children and returns error -81881 if any child's `getNumberOfTuples() > (maxValue + 1)`; if all pass, calls `cellFeatureAttrMat.resizeTuples({maxValue + 1})` which cascades to all AM children via `AttributeMatrix::resizeTuples`; otherwise skip the grow block entirely; **(b) `CopyCellDataFunctor` per-cell copy loop** — cancel check, `featureFirstCellOffset` flat-vector first-encounter recording, per-component value comparison with one-warning-only guard, last-writer-wins write. +Test names omit the common `SimplnxCore::CreateFeatureArrayFromElementArrayFilter: ` prefix. AF numbers refer to the full names in Test inventory. | # | Phase | Path | Test case | -|---|---|---|---| -| 1 | (a) Guard | `featureIdsRef.getNumberOfTuples() == 0` → `MakeErrorResult(-81882, ...)` | AF-7: featureIds array with 0 tuples; preflight succeeds; execute returns error -81882 | -| 2 | (a) Guard | `minValue < 0` (any feature ID negative — covers all-negative and mixed) → `MakeErrorResult(-81880, ...)` | AF-4: `featureIds = [-1, -2, -1]`; AF-8: `featureIds = [-1, 1, 2, -1]`; preflight succeeds; execute returns error -81880 | -| 3 | (a) Grow — shrink guard | `maxValue + 1 > AM.tupleCount` AND some AM child array has `getNumberOfTuples() > (maxValue + 1)` → `MakeErrorResult(-81881, ...)` — refuses to grow AM if doing so would shrink an independently oversized child | AF-5: Feature AM with 2 tuples; sibling child created directly with 5 tuples; `featureIds` max=3 → `maxValue+1=4`; outer fires (4>2), sibling check fires (5>4); execute returns error -81881 | -| 4 | (a) Grow | `maxValue + 1 > AM.tupleCount`, shrink check passes → `cellFeatureAttrMat.resizeTuples({maxValue + 1})` cascades to all AM children including `createdArray` | AF-1 (AM 1→3), AF-2 (AM 1→3), AF-3 (AM 1→3), AF-6 (AM 1→3) — all fixtures start with a 1-tuple AM; output `getNumberOfTuples()` asserted via Class 4 invariant | -| 5 | (a) Skip | `maxValue + 1 <= AM.tupleCount` → grow block skipped | `Valid filter execution - 1 Component`, `Valid filter execution - 3 Component` — Small IN100 AM already correctly sized | -| 6 | (b) Cancel | `if(shouldCancel) return {}` at top of per-cell loop | *Not tested. Cancel-signal injection not implemented in any test fixture.* | -| 7 | (b) Per-cell | First encounter for a `featureId`: record in `featureFirstCellOffset`; write value to output | AF-1, AF-2, AF-3, AF-6 (all feature IDs present) | -| 8 | (b) Per-cell | Subsequent encounter, values match first-seen: no warning emitted; write value | AF-1 (features 1 and 2), AF-3 (features 1 and 2) | -| 9 | (b) Per-cell | Subsequent encounter, values differ: one warning total (guarded by `result.warnings().empty()`); write current value (last-writer-wins) | AF-2 (feature 1: `10.0` vs `15.0`) | -| 10 | (b) Per-cell | Multi-component inner loop: `totalCellArrayComponents > 1` — inner `for(cellCompIdx)` iterates C times per cell | AF-3 (C=3, uint8) | -| 11 | (preflight) Tuple count mismatch | `selectedCellArrayStore.getNumberOfTuples() != featureIdsArray.getNumberOfTuples()` → `MakePreflightErrorResult(-81883, ...)` | AF-9: cellArray 4 tuples, featureIds 2 tuples; preflight returns error -81883 | +|---|-------|------|-----------| +| 1 | Preflight | Unequal element and FeatureIds tuple counts → `-81883`. | `AF-9 error path featureIds tuple count mismatch` | +| 2 | Preflight | Equal tuple counts → output action preserves input type and component shape. | `AF-1 single-component consistent`; `AF-3 three-component consistent` | +| 3 | Validation | Empty FeatureIds → `-81882`. | `AF-7 error path empty featureIds array` | +| 4 | Validation | Negative FeatureId → `-81880`. | `AF-4 error path all-negative featureIds`; `AF-8 error path mixed negative and positive featureIds` | +| 5 | Validation | Scan a full ID chunk and a partial tail to find the maximum. | `Bulk last-value boundary oracle` — 65,538 tuples | +| 6 | Validation | ID scan read fails → propagate error. | *Not directly tested. No fixture injects an ID-scan read failure.* | +| 7 | Validation | Cancel during or after the maximum-ID scan → return. | *Not directly tested. No fixture injects cancellation during validation.* | +| 8 | Sizing | Growing the matrix would shrink a larger child → `-81881`. | `AF-5 error path shrink-protection guard` | +| 9 | Sizing | Required feature count exceeds matrix size → grow matrix and children. | AF-1, AF-2, AF-3, AF-6; both `Valid filter execution` cases | +| 10 | Sizing | Matrix already has enough tuples → skip resizing. | *Not directly tested. All successful execution fixtures begin with a one-tuple feature matrix and require growth.* | +| 11 | Sizing | Matrix child is not an array → skip child-size check. | *Not directly tested. Fixtures contain array children only.* | +| 12 | Sizing | Matrix resize fails → propagate error. | *Not directly tested. No fixture injects a resize failure.* | +| 13 | Copy | Read a full element/ID chunk and a partial tail. | `Bulk last-value boundary oracle` | +| 14 | Copy | FeatureIds read fails → propagate error. | *Not directly tested. No fixture injects a copy-stage ID read failure.* | +| 15 | Copy | Element read fails → propagate error. | *Not directly tested. No fixture injects an element read failure.* | +| 16 | Copy | Cancel between chunks → return before the final write. | *Not directly tested. No fixture injects cancellation during copying.* | +| 17 | Copy | First encounter of a feature → cache its first tuple. | AF-1, AF-3; `Bulk last-value boundary oracle` — feature 1 first appears in the final tuple | +| 18 | Copy | Later tuple equals the first tuple → no warning. | AF-1, AF-3, AF-6 | +| 19 | Copy | Later tuple differs → warning `-1000`; keep its last value. | AF-2; `Bulk last-value boundary oracle` | +| 20 | Copy | More components differ after the first warning → suppress additional warnings. | `Bulk last-value boundary oracle` — three conflicting components, one warning | +| 21 | Output | Unused feature tuples, including an unmapped background, remain zero. | AF-2, AF-3, AF-6; `Bulk last-value boundary oracle` | +| 22 | Output | Write the completed feature buffer successfully. | AF-1, AF-2, AF-3, AF-6; `Bulk last-value boundary oracle` | +| 23 | Output | Final write fails → propagate error. | *Not directly tested. No fixture injects an output write failure.* | +| 24 | Dispatch | float32 scalar, uint8 three-component and int32 three-component copies. | AF-1, AF-3; `Bulk last-value boundary oracle` | +| 25 | Dispatch | Other accepted primitive data types. | *Not directly tested. No value fixture uses bool, int8, int16, uint16, uint32, int64, uint64 or float64.* | ## Test inventory +All cases are in `src/Plugins/SimplnxCore/test/CreateFeatureArrayFromElementArrayTest.cpp`. All pass in CI. + | Test case | Status | Notes | -|---|---|---| -| `Valid filter execution - 1 Component` | kept — regression | Loads Small IN100 via `6_5_test_data_1_v2.tar.gz`; runs filter on `CellData/ConfidenceIndex` (float32, 1-comp); compares output against `CellFeatureData/ConfidenceIndex` pre-existing in the dream3d file. Exercises path #5 (skip-grow: AM already correctly sized). **Circular-oracle flag**: `CellFeatureData/ConfidenceIndex` was generated by DREAM3D 6.5.x using the same-algorithm filter; comparison is legacy-output regression, not an independent oracle. Early-exit loop (breaks on first mismatch) instead of `CompareDataArrays` — all values compared only when all match, which is valid for the regression purpose. | -| `Valid filter execution - 3 Component` | kept — regression | Same as above on `CellData/IPFColors` (uint8, 3-comp). Same circular-oracle flag applies. Exercises paths #5 and #10 in the coverage table (skip-grow and multi-component). | -| `SIMPL Backwards Compatibility` | kept | Verifies `FromSIMPLJson()` mapping from both 6.5 UUID (`94438019-21bb-5b61-a7c3-66974b9a34dc`) and 6.4 filter-name (`CreateFeatureArrayFromElementArray`) formats. 4 parameter assertions each variant. Passes at HEAD. | -| `AF-1 single-component consistent` | implemented | Class 1 + Class 4 oracle; single-component float32, 5 cells, all consistent. Covers paths #4, #7, #8. Inline `REQUIRE` assertions per hand derivation in Oracle section. | -| `AF-2 single-component inconsistent` | implemented | Class 1 + Class 4 oracle; single-component float32, 4 cells, feature-1 inconsistent. Covers paths #4, #7, #9; verifies `warnings().size() == 1`. Inline `REQUIRE` assertions. | -| `AF-3 three-component consistent` | implemented | Class 1 + Class 4 oracle; 3-component uint8, 4 cells, all consistent. Covers paths #4, #7, #8, #10. Inline `REQUIRE` assertions per hand derivation. | -| `AF-4 error path all-negative featureIds` | implemented | Class 4 oracle (error invariant); `featureIds = [-1, -2, -1]`; preflight succeeds; execute returns error -81880. Covers path #2. | -| `AF-5 error path shrink-protection guard` | implemented | Class 4 oracle (error invariant); Feature AM 2 tuples, sibling child created with 5 tuples, `featureIds` max=3; preflight succeeds; execute returns error -81881. Covers path #3. | -| `AF-6 gap in FeatureIds range (resize-grown tuple never written)` | implemented | Class 1 + Class 4 oracle; float32, `featureIds = [0, 2, 0, 2]` (feature 1 never mapped); AM grows 1→3; output[1] retains init value 0.0 (`m_InitValue` fill from `DataStore` construction). Covers paths #4, #7, #8. | -| `AF-7 error path empty featureIds array` | implemented | Class 4 oracle (error invariant); featureIds array with 0 tuples; preflight succeeds; execute returns error -81882. Covers path #1. | -| `AF-8 error path mixed negative and positive featureIds` | implemented | Class 4 oracle (error invariant); `featureIds = [-1, 1, 2, -1]`; maxValue=2 passes old all-negative guard; `minValue < 0` guard catches it; preflight succeeds; execute returns error -81880. Covers path #2. | -| `AF-9 error path featureIds tuple count mismatch` | implemented | Class 4 oracle (error invariant); cellArray 4 tuples, featureIds 2 tuples (in separate smaller AM); preflight returns error -81883. Covers path #11. | +|-----------|--------|-------| +| `SimplnxCore::CreateFeatureArrayFromElementArrayFilter: Valid filter execution - 1 Component` | kept | Loads Small IN100, grows a one-tuple matrix and compares float32 ConfidenceIndex with the stored feature array using `CompareDataArrays`. | +| `SimplnxCore::CreateFeatureArrayFromElementArrayFilter: Valid filter execution - 3 Component` | kept | Loads Small IN100, grows a one-tuple matrix and compares all uint8 IPFColors components with the stored feature array. | +| `SimplnxCore::CreateFeatureArrayFromElementArrayFilter: AF-1 single-component consistent` | new-for-V&V | Checks `[5,10,20]`, no warnings, three output tuples and matching type/component count. | +| `SimplnxCore::CreateFeatureArrayFromElementArrayFilter: AF-2 single-component inconsistent` | new-for-V&V | Checks `[0,15,20]`, exactly one warning, three tuples and matching type/component count. | +| `SimplnxCore::CreateFeatureArrayFromElementArrayFilter: AF-3 three-component consistent` | new-for-V&V | Checks all nine uint8 values against `[[0,0,0],[10,20,30],[40,50,60]]`, no warnings and matching type/component count. | +| `SimplnxCore::CreateFeatureArrayFromElementArrayFilter: AF-4 error path all-negative featureIds` | new-for-V&V | Uses IDs `[-1,-2,-1]`; expects execution error `-81880`. | +| `SimplnxCore::CreateFeatureArrayFromElementArrayFilter: AF-5 error path shrink-protection guard` | new-for-V&V | Uses a two-tuple matrix with a five-tuple child and required size four; expects `-81881`. | +| `SimplnxCore::CreateFeatureArrayFromElementArrayFilter: AF-6 gap in FeatureIds range (resize-grown tuple never written)` | new-for-V&V | Uses IDs `[0,2,0,2]`; checks `[5,0,30]`, no warnings, growth to three tuples and matching type/component count. | +| `SimplnxCore::CreateFeatureArrayFromElementArrayFilter: AF-7 error path empty featureIds array` | new-for-V&V | Uses empty element and ID arrays; expects execution error `-81882` after successful preflight. | +| `SimplnxCore::CreateFeatureArrayFromElementArrayFilter: AF-8 error path mixed negative and positive featureIds` | new-for-V&V | Uses IDs `[-1,1,2,-1]`; expects execution error `-81880`. | +| `SimplnxCore::CreateFeatureArrayFromElementArrayFilter: AF-9 error path featureIds tuple count mismatch` | new-for-V&V | Uses four element tuples and two ID tuples; expects preflight error `-81883`. | +| `SimplnxCore::CreateFeatureArrayFromElementArrayFilter: Bulk last-value boundary oracle` | kept | Checks 12 int32 output values across a 65,536-tuple boundary and a two-tuple tail, a gap feature, and exactly one warning `-1000`. | +| `SimplnxCore::CreateFeatureArrayFromElementArrayFilter: SIMPL Backwards Compatibility` | kept | Variants `SIMPL 6.5 (UUID)` and `SIMPL 6.4 (Filter_Name)` check filter identity, empty comments and four converted parameter values. | ## Exemplar archive -- **Archive:** `6_5_test_data_1_v2.tar.gz` (shared input archive — not an oracle-specific archive for this filter) +- **Archive:** `6_5_test_data_1_v2.tar.gz` - **SHA512:** `585b51ba1da9784a204fe88073ca562b45afd7007cf451b0193079b885c4b4caff7cf21b13e016433b84155546ac0f73f003a8b8ebb1c58360b2c56de3027d6c` - **Provenance:** `src/Plugins/SimplnxCore/vv/provenance/CreateFeatureArrayFromElementArrayFilter.md` -- **Oracle note:** The Class 1 and Class 4 oracles are encoded entirely as inline `REQUIRE` assertions in the AnalyticalFixtures test cases. No oracle-specific exemplar archive is created or required; `6_5_test_data_1_v2.tar.gz` provides regression test input data only. -## Deviations from DREAM3D 6.5.171 +## Deviations from DREAM.3D v6.5.171 -See `vv/deviations/CreateFeatureArrayFromElementArrayFilter.md`. +See `vv/deviations/CreateFeatureArrayFromElementArrayFilter.md` for the root cause, affected users and recommendation. This table lists only Part 1 entries. -**Legacy comparison complete (2026-07-23) — one deviation, no migration impact.** Empirical A/B comparison was bit-identical on the Exact Match fixture. Static source analysis identified one deviation (D1): SIMPLNX handles the AM under-sized case (`max(featureIds) + 1 > AM.tupleCount`) by resizing the AM to accommodate, while SIMPL errors with -5555. For the Exact Match and AM over-sized cases — the only cases reachable from a SIMPL pipeline that ran successfully — SIMPLNX output is bit-identical to SIMPL. See `vv/deviations/CreateFeatureArrayFromElementArrayFilter.md` for full analysis. +| Deviation | Observed difference | +|-----------|---------------------| +| `CreateFeatureArrayFromElementArray-D1` | SIMPLNX grows an undersized feature matrix and preserves an oversized matrix. DREAM.3D v6.5.171 rejects both size mismatches. | +| `CreateFeatureArrayFromElementArrayFilter-D2` | SIMPLNX rejects negative FeatureIds before copying; DREAM.3D v6.5.171 can access memory outside the output array. | +| `CreateFeatureArrayFromElementArrayFilter-D3` | SIMPLNX rejects unequal element and FeatureIds tuple counts; DREAM.3D v6.5.171 does not check this condition. | diff --git a/src/Plugins/SimplnxCore/vv/DBSCANFilter.md b/src/Plugins/SimplnxCore/vv/DBSCANFilter.md index 89dc8ed1e8..a466727924 100644 --- a/src/Plugins/SimplnxCore/vv/DBSCANFilter.md +++ b/src/Plugins/SimplnxCore/vv/DBSCANFilter.md @@ -1,467 +1,128 @@ # V&V Report: DBSCANFilter -| | | -|----------------------------|-------------------------------------------------------------------------------------------| -| Plugin | SimplnxCore | -| SIMPLNX UUID | `763dad44-fad7-4606-808f-617867257b98` | -| SIMPLNX Human Name | DBSCAN | -| DREAM3D 6.5.171 equivalent | `DBSCAN` (SIMPL UUID `c2d4f1e8-2b04-5d82-b90f-2191e8f4262e`) — legacy UUID mapped in `SimplnxCoreLegacyUUIDMapping.hpp` | -| Verified commit | ** | -| Status | COMPLETE | -| Sign-off | *Nathan Young, 8/7/2026* second engineer: *Michael Jackson <mike.jackson@bluequartz.net>, 08-28-2026* | + + +| | | +|--------|--------------| +| Plugin | SimplnxCore | +| SIMPLNX UUID | `763dad44-fad7-4606-808f-617867257b98` | +| SIMPLNX Human Name | DBSCAN | +| DREAM.3D v6.5.171 equivalent | `DBSCAN` (DREAM3DReview plugin, SIMPL UUID `c2d4f1e8-2b04-5d82-b90f-2191e8f4262e`) | +| Verified commit | ** | +| Status | COMPLETE | +| Sign-off | Nathan Young, 2026-08-07; second engineer: Michael Jackson, 2026-08-28 | ## At a glance -| Aspect | Current state | -|------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Algorithm Relationship | **Rewrite** — SIMPLNX implements GDCF (Grid-based DBSCAN, Boonchoo et al. 2019, DOI 10.1016/j.patcog.2019.01.034) in place of the traditional point-by-point DBSCAN in legacy DREAM3D. UUID changed from `c2d4f1e8` to `763dad44` (legacy UUID retained via SIMPL mapper). | -| Oracle (confirmed) | **Class 2 (Reference — scikit-learn 1.7.1 DBSCAN) primary + Class 4 (Invariant) companion.** Input data independently generated from deterministic sklearn scripts in `dbscan_vv/dbscan_data_proj/`. Phase 6 reconciliation complete: 4/6 datasets exact match; 2 deviations (ansio, varied) fully explained by DBSCAN-D1 (GDCF vs. traditional DBSCAN). See Phase 5 + Phase 6. | -| Code paths enumerated | 18 of 21 enumerated paths exercised. The untested invalid-component, unvisited-border, and empty-forest paths remain identified below. | -| Tests today | 12 registered tests in this file plus 1 hidden HDF5 batch-tail test. The OOC build also registers seven external-storage tests. Existing analytical and regression assertions are preserved. | -| Exemplar archive | **`dbscan_test.tar.gz` — promoted to regression fixtures (Phase 6/10).** Originally circular oracle; independently verified via Class 2 sklearn oracle (Phase 6). LDF arrays now pin verified-correct SIMPLNX output. See provenance sidecar. | -| Legacy comparison | Historical comparison used a local legacy proof build and the sklearn oracle. It supports the documented GDCF-versus-traditional DBSCAN differences. This OOC recertification does not establish a new stock DREAM3D 6.5.171 binary comparison. | -| Bug flags | ✅ Circular oracle resolved (Phase 6) — `dbscan_test.tar.gz` LDF arrays promoted to regression fixtures; Class 2 sklearn oracle confirms correctness. 2 expected GDCF deviations (ansio, varied) documented as DBSCAN-D1. **1 SIMPLNX bug found and fixed during V&V:** `ParseOrder::Random` fed the user-supplied seed to the shuffle instead of the documented time-based seed, making "Random" a silent duplicate of "Seeded Random" (see Phase 7). **1 robustness defect found and fixed:** an all-false mask left the grid bounds NaN and those NaNs were cast to `usize` while computing grid dimensions (undefined behavior). | -| V&V phase | The original COMPLETE status and named sign-off are retained. OOC recertification adds an analytical ExternalGDCF boundary test and paired runtime checks. No new stock DREAM3D 6.5.171 binary comparison is claimed. | +| Aspect | Current state| +|----------|--------------| +| Algorithm Relationship | Rewrite. The legacy equivalent is `DBSCAN` in DREAM.3D v6.5.171. SIMPLNX uses grid-based DBSCAN (GDCF), so a core object is a grid cell, not a point. | +| Oracle (confirmed) | Class 2 (scikit-learn 1.7.1 DBSCAN), with Class 1 and Class 4 companions. Encoded in 15 registered test cases in `test/DBSCANTest.cpp`. All pass in CI. One hidden OOC test was not run. | +| Code paths enumerated | 18 of 23 exercised. Two paths require the hidden OOC test. Three other paths are guarded or have no isolating fixture. | +| Tests today | 15 registered test cases plus 1 hidden OOC test: 6 two-dimensional sklearn datasets, 1 three-dimensional regression case, 4 analytical fixtures, 3 preflight rejections, 1 preflight allocation check and 1 SIMPL conversion test. | +| Exemplar archive | `dbscan_test.tar.gz` gives inputs and outputs. Four 2D fixtures match the sklearn cluster counts and sizes; two differ as designed. The 3D label array has no external oracle. | +| Legacy comparison | Not run with the current algorithm. The saved comparison uses SIMPLNX exemplar labels; a comparison with current execution output is required. | +| Bug flags| `DBSCANFilter-D4`. Open: `upgradeParametersImpl()` is not implemented, so pipelines saved with parameter version 1 lose a non-default parse order. | +| V&V phase| Oracle review and registered-test checks are complete. The current-algorithm legacy comparison and OOC execution remain. The OOC test suite was not run, because CI does not build the OOC configuration. | ## Summary -`DBSCANFilter` implements Grid-based DBSCAN (GDCF) as described in Boonchoo et al. 2019 — a substantially different algorithm from the traditional point-by-point DBSCAN present in legacy DREAM3D 6.5.171. The SIMPLNX rewrite (PR #1421) replaced the entire implementation; the filter UUID was simultaneously changed from `c2d4f1e8` to `763dad44`, explicitly signaling algorithmic non-equivalence. The critical difference is that SIMPLNX defines a "core object" at the grid-cell level (≥ minPoints data points within a single grid cell) rather than at the individual-point level (≥ minPoints data points within an ε-ball of a given point), producing different clustering results for sparse datasets where points individually have neighbors within ε but those neighbors span multiple grid cells. V&V is complete: (a) circular exemplar oracle resolved via independent Class 2 sklearn oracle (Phase 6) and legacy comparison (Phase 9); (b) Class 4 invariants encoded in tests (Phase 8); (c) two expected GDCF deviations documented as DBSCAN-D1/D2. Pending: second-engineer oracle review. +`DBSCANFilter` puts points into clusters by density with the GDCF algorithm (Boonchoo et al. 2019), which defines core objects on grid cells. Verification uses a Class 2 scikit-learn oracle, Class 1 fixtures and Class 4 invariants. Four of six 2D exemplar datasets match the reference cluster counts and sizes, two differ as designed (`DBSCANFilter-D1`), and the V&V found and fixed four SIMPLNX defects. ## Algorithm Relationship -*Classification:* **Rewrite** +*Classification:* Rewrite -*Evidence:* PR #1421 ("PERF/ENH: DBSCAN Rewrite", commit `c1bc9114b7`, 2025-09-04) replaced the entire `DBSCAN.cpp` implementation with a GDCF-based approach derived from Boonchoo et al. 2019. The filter UUID was simultaneously changed from `c2d4f1e8-2b04-5d82-b90f-2191e8f4262e` to `763dad44-fad7-4606-808f-617867257b98`, explicitly signaling that the new filter is not a line-for-line translation of the legacy algorithm. The legacy UUID is retained in `SimplnxCoreLegacyUUIDMapping.hpp` for pipeline backward compatibility, routing old SIMPL pipelines through `FromSIMPLJson` to populate the new parameters. - -*Algorithmic change summary (GDCF vs. traditional DBSCAN):* - -1. **Core-object definition** — Legacy: a data point is a core point if ≥ minPoints data points lie within its ε-neighborhood (inclusive of itself). SIMPLNX: a grid cell is a core grid if it contains ≥ minPoints data points. This is the fundamental behavioral difference — see Deviation DBSCAN-D1. -2. **Grid-based spatial indexing** — SIMPLNX bins points into a regular grid with cell side length `ε/sqrt(dims)`, builds per-dimension bit-packed adjacency tables (HyperGridBitMap), and queries nearest-neighbor grids via bitwise AND. Legacy performed direct point-to-point ε-ball queries on every pair. -3. **Union-find cluster forest** — SIMPLNX uses a forest of `ClusterNode` structs with union-by-min-cluster-id to merge density-reachable grids. Legacy used a queue-based expansion loop. -4. **Parse order choices** — SIMPLNX adds `LowDensityFirst` (sort core grids ascending by grid occupancy) and `SeededRandom` (deterministic shuffle). Legacy had no equivalent of `LowDensityFirst`. -5. **`use_precaching` removed** — Legacy had a memory/time trade-off switch. SIMPLNX's GDCF made it irrelevant; parameter removed in v2 (SIMPL converter silently ignores it via `parametersVersion() == 2`). -6. **Parameter rename** — `init_type_index` → `parse_order_index`. Handled in `FromSIMPLJson` reading old key into new parameter. - -*Material PRs since baseline:* - -- **#994** — "FILTER/ENH: DBSCAN Filter and Clustering Cleanup" (2024-06-21) — Original traditional DBSCAN implementation. -- **#1421** — "PERF/ENH: DBSCAN Rewrite" (2025-09-04, commit `c1bc9114b7`) — Complete algorithm replacement with GDCF. UUID changed; new 2D/3D test cases added; documentation rewritten. -- **#1576** — "ENH: Improve error messages across the codebase" (commit `f885a0ebc9`) — Error message text edits in `DBSCAN.cpp`. No algorithmic change. +*Evidence:* DREAM3D-NX v7.2.2 replaced point-by-point DBSCAN with GDCF. Core identification uses grid-cell occupancy instead of the point-level neighborhood count. ## Oracle -*Class:* **2 (Reference — scikit-learn 1.7.1)** primary + **Class 1 (Analytical)** for error paths + **Class 4 (Invariant)** companion - -*Applied:* - -**Class 2** — scikit-learn 1.7.1 DBSCAN run on the same 6 sklearn toy input datasets, same ε/minPts parameters. Comparison at cluster-structure level (count + sorted bin sizes). Results in `dbscan_vv/oracle_results.json`. 4/6 exact match; 2 deviations (ansio, varied) explained by DBSCAN-D1. See Phase 5 + Phase 6. - -**Class 1** — Hand-derived analytical fixtures for SIMPLNX-specific code paths not reachable by the Class 2 sklearn oracle: -- **F1 (no-clusters warning)**: 4 corner points, ε=0.1, minPts=5 → each point in its own cell → warning -85640, all IDs=0. -- **F2 (mask exclusion)**: 3 points, P2 masked → P0+P1 in one core cell (ε=1.0, minPts=2) → ClusterIds=[1,1,0]. -Both implemented in `DBSCANTest.cpp`. - -**Class 4** — Structural invariants asserted on every test run via `CheckClusterInvariants()`: -- `ClusterIds[i] >= 0` for all i -- IDs 1..max(ClusterIds) contiguous (no gaps — unlabeled 0 is exempt) -- `AttributeMatrix.tupleCount == max(ClusterIds) + 1` +*Class:* 2 (Reference — scikit-learn 1.7.1 `sklearn.cluster.DBSCAN`), with 1 (Analytical) and 4 (Invariant) companions. -*Note*: the invariant "No masked points have ClusterIds > 0" is spot-checked in F2 but **not** encoded as a general assertion in `CheckClusterInvariants()` (which does not receive the mask path). If mask coverage is later added to the main test helpers, this invariant should be added there. +*Applied:* scikit-learn ran on the six sklearn input datasets with the same epsilon and minimum points as the tests. The comparison uses the cluster count and the sorted cluster sizes. Hand-derived fixtures give exact labels for the paths that scikit-learn cannot reach. `CheckClusterInvariants()` checks that IDs are not negative, that IDs 1 to N have no gaps, and that the AttributeMatrix has N + 1 tuples. -*Encoded:* **Complete (Phase 8, 2026-08-05).** Class 4 invariants in `CheckClusterInvariants()` hooked into all 2D test helpers. Class 1 F1/F2 added as dedicated TEST_CASEs. 3D test lacks `CheckClusterInvariants` (no mask; AM tuple check present inline). +*Encoded:* `src/Plugins/SimplnxCore/test/DBSCANTest.cpp` — 6 Class 2 regression fixtures (`2D Test: *`), 4 Class 1 fixtures (`Analytical Fixture F1`, `F2`, `F3`, `genuine HDF5 65536-batch tail oracle`) and Class 4 invariants in the 2D helpers and F1 to F3. The CI result covers the registered tests. All pass in CI. The hidden OOC fixture was not run. -*Second-engineer review:* *Pending — see Phase 4 and Phase 13.* +*Second-engineer review:* *Michael Jackson, 2026-08-28* ## Bugs found and fixed -The existing fixes remain in the branch. This boundary recertification changes no production algorithm. +All defects in this table are fixed. The fixes will be in the DREAM3D-NX release after v7.4.2. DREAM3D-NX v7.4.2 contains the fix for `DBSCANFilter-D5`, `DBSCANFilter-D7` and `DBSCANFilter-D8`. -| Deviation | Defect | Affected released versions | Resolution in this branch | -|-----------|--------|----------------------------|---------------------------| -| `DBSCAN-D5` | The old shuffle excluded an index and used a biased partner range. | GDCF-era DREAM3D-NX releases before the recorded August 2026 correction. The historical sidecar does not enumerate release numbers. | The existing unbiased shuffle correction is retained. | - -The prior Random-seed and all-false-mask fixes are described in Phase 7. They have no stable deviation IDs in the existing sidecar; this update does not invent IDs or release coverage. +| Deviation | Defect | Affected released versions | Resolution | +|-----------|--------|----------------------------|------------| +| `DBSCANFilter-D5` | The `SeededRandom` shuffle was biased. It never selected the last index, and it drew partners from the full range. | DREAM3D-NX v7.2.2 through v7.4.1. DREAM.3D v6.5.171 was not affected. | `std::shuffle` replaces the hand-written shuffle. | +| `DBSCANFilter-D6` | The filter accepted a multi-component mask or a mask with fewer tuples than the input. The in-core algorithm then read wrong mask values or read past the end of the mask. | DREAM3D-NX v7.0.0 through v7.4.2. | Preflight rejects a mask with a component shape other than `{1}` (`-208`) and a mask with a different tuple count (`-7587`). | +| `DBSCANFilter-D7` | The `Random` parse order used the user seed, not a time-based seed. `Random` gave the same result as `SeededRandom` with the default seed. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | The filter gives the generated seed to the algorithm. | +| `DBSCANFilter-D8` | A mask with no true values left the grid bounds as NaN. The algorithm then cast NaN to `usize`, which is undefined behavior. | DREAM3D-NX v7.2.2 through v7.4.1. DREAM.3D v6.5.171 was not affected. | An explicit check returns warning `-85640` with a message about the mask. | ## Code path coverage -18 of 21 enumerated paths exercised. - - -Source: `Algorithms/DBSCAN.cpp` (34 lines), `DBSCANDirect.cpp` (1,274 lines), and `DBSCANScanline.cpp` (3,655 lines). Scanline selects ExternalGDCF when a routed array is on disk; forced Scanline on resident arrays uses its resident fallback. - -Logical phases: **(a) Grid construction** — build HyperGridBitMap and bin points; **(b) Core identification** — find and sort core grids; **(c) Cluster phase** — union-find merge of core/border grids; **(d) Expansion** — iterative border-grid expansion loop; **(e) Cleanup + Label** — renumber cluster IDs, assign to points. - -| # | Phase | Path | Test case | -|----|---------------|----------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------| -| 1 | (a) Grid 2D | Input has 2 components → `HyperGridBitMap2D` path in `DBSCANFunctor` | All 2D tests (Aniso, Blobs, Circles, Moons, NoStructure, Varied) | -| 2 | (a) Grid 3D | Input has 3 components → `HyperGridBitMap3D` path in `DBSCANFunctor` | `3D Test (LowDensityFirst)` | -| 3 | (a) Grid err | Input has other component count → error `-54060` | *Not directly tested. Preflight rejects via `AllowedComponentShapes{{2},{3}}`.* | -| 4 | (a) Mask=true | Masked points skipped in binning and bounds | **F2** (`Analytical Fixture F2 - Mask Exclusion`, added Phase 8) | -| 4b | (a) Mask all false | No point passes the mask → bounds stay NaN → early return leaves the grid empty → warning `-85640` | **F3** (`Analytical Fixture F3 - All Points Masked`, added during Phase 7 fix) | -| 5 | (b) No cores | All grids have

/test/CMakeLists.txt`. +## Archive identity + +| Field | Value | +|---|---| +| **Archive** | `6_6_stats_test_v2.tar.gz` | +| **SHA512** | `e84999dec914d81efce4fc4237c49c9bf32e48381b1e79f58aa4df934f0d7606cd7a948f9a5e7b17a126a7944cc531b531cfdc70756ca3e2207b20734e089723` | +| **Used by tests** | `SimplnxCore::ComputeFeatureSizes: Legacy: Small IN100 Test` | +| **Generated by** | Jared Duffey (inferred from PR #1124) | +| **Generated on** | 2024-11-01 to 2025-02-07 (inferred from PR #1124) | +| **Generated at commit** | `dc4544512` (PR #1124 merge, inferred) | ---- +The archive name and SHA512 match `src/Plugins/SimplnxCore/test/CMakeLists.txt:248`. -## Archive identity +The inline fixtures are separate from the archive: | Field | Value | |---|---| -| **Archive** | `Inlined - CreateRectGridDataStructure()` | -| **SHA512** | `N/A` | -| **Used by tests** | `Valid: Rectilinear Grid`, `Valid: Rectilinear Grid with Element Size` | +| **Builders** | `CreateRectGridDataStructure()`, `Create2DImageDataStructure()`, `Create3DImageDataStructure()` | +| **Used by tests** | The six `SimplnxCore::ComputeFeatureSizes: Valid:` cases | | **Generated by** | *Nathan Young* | | **Generated on** | *2026-02-20* | -| **Generated at commit** | `34c86ab4e419725728ddb6270119bb40ea4c2945` | +| **Generated at commit** | `34c86ab4e419725728ddb6270119bb40ea4c2945` (PR #1540) | +| **Source** | `src/Plugins/SimplnxCore/test/ComputeFeatureSizesTest.cpp` | ## How it was generated -The `CreateRectGridDataStructure()` function generates a 4x4x4 Rectilinear Grid Geometry and all descendant arrays on each test invocation. The actual inlined data in the function was synthesized by hand to test typical functionality. The expected output is a Class 1 Oracle as it is hand derived. +The Small IN100 regression test loads `6_6_stats_test_v2.dream3d` from the archive. It compares the generated `Volumes`, `EquivalentDiameters`, and `NumElements` arrays with the archived arrays. These archived arrays supply regression expectations; the independent Class 1 expectations come from the inline fixtures below. + +The three builders create synthetic geometries and hand-specified FeatureIds on each test invocation. Their Image inputs have zero origin and equal cell counts along the non-flat axes, but different axis spacings and feature counts. `CreateFeatureOutputDataStructure()` adds three output-block variants, including background and unused features. Its Image 2D expectations use the current slab formula. -Relevant Information for the data set: +**RectGrid input** + +The `CreateRectGridDataStructure()` function generates a 4x4x4 Rectilinear Grid Geometry and all descendant arrays on each test invocation. The inputs are hand-specified. The expected output is a Class 1 hand derivation. + +Input values: ```text Image Info: @@ -52,15 +67,83 @@ Feature Ids: 3, 2, 1, 2 ``` +**Image 2D input** + +The `Create2DImageDataStructure()` function generates a 5x5x1 Image Geometry and all descendant arrays on each test invocation. The inputs are hand-specified and include a single-cell feature. The expected output is a Class 1 hand derivation. + +Input values: + +```text +Image Info: +Dimensions: {5,5,1} +Spacing: {20.2f, 0.1f, 1.0f} +Origin: {0.0f, 0.0f, 0.0f} + +Feature Ids: +1, 2, 3, 3, 3, +1, 1, 1, 1, 1, +1, 1, 1, 3, 3, +3, 3, 1, 1, 3, +3, 3, 3, 3, 3 +``` + +**Image 3D input** + +The `Create3DImageDataStructure()` function generates a 5x5x5 Image Geometry and all descendant arrays on each test invocation. The inputs are hand-specified. The expected output is a Class 1 hand derivation. + +Input values: + +```text +Image Info: +Dimensions: {5,5,5} +Spacing: {1.2f, 0.9f, 2.1f} +Origin: {0.0f, 0.0f, 0.0f} + +Feature Ids: +1, 2, 2, 2, 2, +1, 1, 1, 1, 1, +1, 1, 1, 2, 2, +2, 2, 1, 1, 2, +2, 2, 2, 2, 2, + +1, 1, 1, 1, 1, +1, 1, 1, 1, 1, +1, 1, 1, 1, 1, +1, 1, 1, 1, 1, +1, 1, 1, 1, 1, + +3, 3, 3, 3, 1, +1, 3, 1, 3, 3, +2, 2, 1, 1, 1, +2, 2, 1, 1, 3, +2, 2, 1, 3, 3, + +3, 2, 2, 1, 1, +3, 2, 1, 1, 1, +3, 1, 1, 1, 1, +3, 2, 1, 2, 1, +3, 2, 2, 2, 2, + +3, 1, 3, 1, 1, +1, 1, 1, 1, 1, +3, 1, 1, 1, 3, +1, 1, 1, 3, 1, +3, 1, 3, 1, 3 +``` + ## Canonical oracle output +The Class 1 oracle lives in the test code. The following paths refer to the inline fixtures, not the regression archive. + | DataPath | Source of expected values | |---|---| -| `Image/FeatureData/NumElements` | *Class 1 hand derivation* | -| `Image/FeatureData/Volumes` | *Class 1 hand derivation* | -| `Image/FeatureData/EquivalentDiameters` | *Class 1 hand derivation* | +| `Image/FeatureData/NumElements` | Class 1 count of cells with each FeatureId | +| `Image/FeatureData/Volumes` | Class 1 cell count times cell area or volume, or sum of RectGrid cell volumes | +| `Image/FeatureData/EquivalentDiameters` | Class 1 circle or sphere formula applied to the feature area or volume | -The expected outputs are as follows: +The decimal derivations below explain the expected values. `Validate2DImageDataStructure()`, `Validate3DImageDataStructure()`, and `ValidateRectGridDataStructure()` contain the executable float32 expectations with relative tolerance `1e-6`. All registered tests pass in CI at `01809c236` on Linux, macOS, and Windows. The flat-spacing case is an expected failure (`[!shouldfail]`): it expects area 24 for flat X, Y, and Z, while the filter calculates 120 (`ComputeFeatureSizes-D3`). + +**RectGrid derivation** ```text NumElements: {0, 39, 15, 10} @@ -68,7 +151,7 @@ Volumes: {0.0, 2362.434, 352.462, 15.104} EquivalentDiameters: {0.0, 16.5242, 8.764, 3.0669} ``` -In order to derive these values, the voxels must be processed individually and summed up. This is how it was done: +Sum the cell volumes for each FeatureId: ```text ======================== @@ -225,90 +308,9 @@ Data ======================== ``` -```text -Calculated Output +Calculate the equivalent spherical diameter from the feature volume `V`: `D = 2*cbrt(3*V/(4*pi))`. - 0.06 0.03 0.12 1.09 - 0.54 0.27 1.08 9.81 - 5.4 2.7 10.8 98.1 - 54 27 108 981 - - 0.012 0.006 0.024 0.218 - 0.108 0.054 0.216 1.962 - 1.08 0.54 2.16 19.62 - 10.8 5.4 21.6 196.2 - - 0.048 0.024 0.096 0.872 - 0.432 0.216 0.864 7.848 - 4.32 2.16 8.64 78.48 - 43.2 21.6 86.4 784.8 - - 0.00599999 0.003 0.012 0.109 - 0.0539999 0.027 0.108 0.980999 - 0.539999 0.27 1.08 9.80999 - 5.39999 2.7 10.8 98.0999 - ``` - -We calculate the `EquivalentDiameters` with the volumes: - -Given that volume of a circle can be derived by `Volume = 4/3 * pi * r^3`, we can isolate the radius given volume. That formula becomes `radius = cubed_root((3 * Volume) / (4 * pi))`, and given that the radius is half of the diameter the formula becomes `Diameter = 2 * cubed_root((3 * Volume) / (4 * pi))`. - -## Oracle provenance (Classes 2, 3, 5 only) - -N/A - -## Second-engineer oracle review - -- **Reviewer:** ** OR *skipped* -- **Date:** *YYYY-MM-DD* -- **Skip reason** (if skipped): ** - -## Regenerated to fix a circular-oracle situation? - -N/A - ---- - -## Archive identity - -| Field | Value | -|---|---| -| **Archive** | `Inlined - Create2DImageDataStructure()` | -| **SHA512** | `N/A` | -| **Used by tests** | `Valid: Image 2D`, `Valid: Image 2D with Element Sizes` | -| **Generated by** | *Nathan Young* | -| **Generated on** | *2026-02-20* | -| **Generated at commit** | `34c86ab4e419725728ddb6270119bb40ea4c2945` | - -## How it was generated - -The `Create2DImageDataStructure()` function generates a 5x5x1 Image Geometry and all descendant arrays on each test invocation. The actual inlined data in the function was synthesized by hand to test typical functionality as well as an isolated voxel edge case. The expected output is a Class 1 Oracle as it is hand derived. - -Relevant Information for the data set: - -```text -Image Info: -Dimensions: {5,5,1} -Spacing: {20.2f, 0.1f, 1.0f} -Origin: {0.0f, 0.0f, 0.0f} - -Feature Ids: -1, 2, 3, 3, 3, -1, 1, 1, 1, 1, -1, 1, 1, 3, 3, -3, 3, 1, 1, 3, -3, 3, 3, 3, 3 -``` - -## Canonical oracle output - -| DataPath | Source of expected values | -|---|---| -| `Image/FeatureData/NumElements` | *Class 1 hand derivation* | -| `Image/FeatureData/Volumes` | *Class 1 hand derivation* | -| `Image/FeatureData/EquivalentDiameters` | *Class 1 hand derivation* | - -The expected outputs are as follows: +**Image 2D derivation** ```text NumElements: {0, 11, 1, 13} @@ -316,111 +318,46 @@ Areas: {0.0, 22.22, 2.02, 26.26} EquivalentDiameters: {0.0, 5.319, 1.6037, 5.782} ``` -In order to derive these values, one first calculates single voxel area which in this case is `2.02`. Then feature counts must be determined using the *Feature Ids* array, this gives the `NumElements` results. With the feature counts, we then determine the size of the feature via multiplying the number of elements in each feature by the voxel area, which gives the `Areas`. Finally, we calculate the `EquivalentDiameters` with the areas: - -Given that area of a circle can be derived by `Area = pi * radius^2`, we can isolate the radius given area. That formula becomes `radius = square_root(Area / pi)`, and given that the radius is half of the diameter the formula becomes `Diameter = 2 * square_root(A / pi)`. - -## Oracle provenance (Classes 2, 3, 5 only) - -N/A - -## Second-engineer oracle review - -- **Reviewer:** ** OR *skipped* -- **Date:** *YYYY-MM-DD* -- **Skip reason** (if skipped): ** - -## Regenerated to fix a circular-oracle situation? - -N/A +Each cell has area `2.02`. Count the cells with each FeatureId to obtain `NumElements`. Multiply each count by `2.02` to obtain the feature area. ---- +Calculate the equivalent circular diameter from the feature area `A`: `D = 2*sqrt(A/pi)`. -## Archive identity - -| Field | Value | -|---|---| -| **Archive** | `Inlined - Create3DImageDataStructure()` | -| **SHA512** | `N/A` | -| **Used by tests** | `Valid: Image 3D`, `Valid: Image 3D with Element Sizes` | -| **Generated by** | *Nathan Young* | -| **Generated on** | *2026-02-20* | -| **Generated at commit** | `34c86ab4e419725728ddb6270119bb40ea4c2945` | - -## How it was generated - -The `Create3DImageDataStructure()` function generates a 5x5x5 Image Geometry and all descendant arrays on each test invocation. The actual inlined data in the function was synthesized by hand to test typical functionality. The expected output is a Class 1 Oracle as it is hand derived. - -Relevant Information for the data set: +**Image 3D derivation** ```text -Image Info: -Dimensions: {5,5,5} -Spacing: {1.2f, 0.9f, 2.1f} -Origin: {0.0f, 0.0f, 0.0f} - -Feature Ids: -1, 2, 2, 2, 2, -1, 1, 1, 1, 1, -1, 1, 1, 2, 2, -2, 2, 1, 1, 2, -2, 2, 2, 2, 2, - -1, 1, 1, 1, 1, -1, 1, 1, 1, 1, -1, 1, 1, 1, 1, -1, 1, 1, 1, 1, -1, 1, 1, 1, 1, - -3, 3, 3, 3, 1, -1, 3, 1, 3, 3, -2, 2, 1, 1, 1, -2, 2, 1, 1, 3, -2, 2, 1, 3, 3, +NumElements: {0, 73, 29, 23} +Volumes: {0.0, 165.564, 65.772, 52.164} +EquivalentDiameters: {0.0, 6.813, 5.008, 4.636} +``` -3, 2, 2, 1, 1, -3, 2, 1, 1, 1, -3, 1, 1, 1, 1, -3, 2, 1, 2, 1, -3, 2, 2, 2, 2, +Each cell has volume `2.268`. Count the cells with each FeatureId to obtain `NumElements`. Multiply each count by `2.268` to obtain the feature volume. -3, 1, 3, 1, 1, -1, 1, 1, 1, 1, -3, 1, 1, 1, 3, -1, 1, 1, 3, 1, -3, 1, 3, 1, 3 -``` +Calculate the equivalent spherical diameter from the feature volume `V`: `D = 2*cbrt(3*V/(4*pi))`. -## Canonical oracle output +**Source comparison and release evidence** -| DataPath | Source of expected values | -|---|---| -| `Image/FeatureData/NumElements` | *Class 1 hand derivation* | -| `Image/FeatureData/Volumes` | *Class 1 hand derivation* | -| `Image/FeatureData/EquivalentDiameters` | *Class 1 hand derivation* | +The legacy source is `Source/Plugins/Statistics/StatisticsFilters/FindSizes.cpp` at the DREAM.3D `v6.5.171` tag. `findSizesUnstructured()` accumulates float32 cell sizes into float32 feature volumes (D1). The diameter calculation reads the stored float32 size and uses `powf` or `sqrtf` (D2). The 2D Image calculation excludes the flat-axis spacing, and both geometry paths use serial loops. DREAM.3D v6.5.171 was not affected by D3, D4, or D5. -The expected outputs are as follows: +The current sources are `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeFeatureSizesDirect.cpp` and `ComputeFeatureSizesScanline.cpp` in the same directory. Both use float64 compensated RectGrid sums and unrounded feature sizes for the diameter. Direct uses thread-local sums; Scanline keeps one compensation value per feature across input chunks. -```text -NumElements: {0, 73, 29, 23} -Volumes: {0.0, 165.564, 65.772, 52.164} -EquivalentDiameters: {0.0, 6.813, 5.008, 4.636} -``` +Each fetched release branch was inspected with `git show upstream/release/v:`. Paths below are relative to `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/`. -In order to derive these values, one first calculates single voxel volume which in this case is `2.268`. Then feature counts must be determined using the *Feature Ids* array, this gives the `NumElements` results. With the feature counts, we then determine the size of the feature via multiplying the number of elements in each feature by the voxel volume, which gives the `Volumes`. Finally, we calculate the `EquivalentDiameters` with the volumes: +| Released versions | Source path | D3: flat spacing | D4: parallel reduction | D5: parallel scheduling | +|---|---|---|---|---| +| DREAM3D-NX v7.0.0 through v7.4.0 | `ComputeFeatureSizesFilter.cpp` | Excluded | Serial loops; defect absent | Serial loops; defect absent | +| DREAM3D-NX v7.4.1 | `Algorithms/ComputeFeatureSizes.cpp` | Excluded | Serial compensated sum; defect absent | Serial loops; defect absent | +| DREAM3D-NX v7.4.2 | `Algorithms/ComputeFeatureSizes.cpp` | Included; defect present | Compensation subtracted from incoming partial sum; fixed | Both FeatureIds residency guards present; fixed | -Given that volume of a circle can be derived by `Volume = 4/3 * pi * r^3`, we can isolate the radius given volume. That formula becomes `radius = cubed_root((3 * Volume) / (4 * pi))`, and given that the radius is half of the diameter the formula becomes `Diameter = 2 * cubed_root((3 * Volume) / (4 * pi))`. +PR #1590 (`e76ad508e`) introduced D3. PR #1592 (`aaad2c4e2`) introduced the parallel code with D4 and D5. PR #1638 (`8d0b0ed22`) corrected the reduction and added both `requireStoresInMemory()` calls. It also changed the six analytical tests to use relative tolerance `1e-6` for platform and reduction-order variation. DREAM3D-NX v7.4.2 contains both fixes. No release from DREAM3D-NX v7.0.0 through v7.4.2 contains D4 or D5. D3 remains in the current Direct and Scanline implementations. ## Oracle provenance (Classes 2, 3, 5 only) -N/A +Not applicable. The independent oracle is Class 1 and is encoded in the test source. ## Second-engineer oracle review -- **Reviewer:** ** OR *skipped* -- **Date:** *YYYY-MM-DD* -- **Skip reason** (if skipped): ** +The report records Michael Jackson's sign-off on 2026-07-28. A separate record of the oracle-design review is required. ## Regenerated to fix a circular-oracle situation? -N/A +The inline Class 1 fixtures are generated from hand-specified inputs and analytical formulas. They do not use archived or filter-generated outputs as their oracle. diff --git a/src/Plugins/SimplnxCore/vv/provenance/ComputeGroupingDensityFilter.md b/src/Plugins/SimplnxCore/vv/provenance/ComputeGroupingDensityFilter.md index 5aa1531f9e..c331a2f73f 100644 --- a/src/Plugins/SimplnxCore/vv/provenance/ComputeGroupingDensityFilter.md +++ b/src/Plugins/SimplnxCore/vv/provenance/ComputeGroupingDensityFilter.md @@ -1,119 +1,109 @@ # Exemplar Archive Provenance: compute_grouping_densities_v2.tar.gz -This sidecar records how the exemplar archive used in `ComputeGroupingDensityFilter`'s Class 1 + Class 4 unit tests was generated. It is the answer to "where did this gold-standard data come from?" - -Pairs with the archive's inline `README.md` (Mike's hand sign-off) and the V&V report at `src/Plugins/SimplnxCore/vv/ComputeGroupingDensityFilter.md`. The inline `README.md` is the materially-complete provenance document; this sidecar exposes the same information at the in-source location the V&V policy expects (`src/Plugins//vv/provenance/.md`). - ---- +This sidecar records the archive inputs, analytical expectations and local proof comparison for `ComputeGroupingDensityFilter`. ## Archive identity -| Field | Value | -|------------------------|------------------------------------------------------------------------------------------------------------------------------------| -| **Archive** | `compute_grouping_densities_v2.tar.gz` | -| **SHA512** | `3aaabb63c4fa16f7fa192ae4ee9dbba9394ec7f1cd19aff55e399a624d495a3a778c7f6f282911f681e85cea99e4c6d15344274e9107f337af7d4a19f93784ff` | -| **Used by tests** | `SimplnxCore::ComputeGroupingDensityFilter: Exemplar A/B — all 4 configurations` (DYNAMIC_SECTION, 4 fixtures `NC0_CF0`, `NC0_CF1`, `NC1_CF0`, `NC1_CF1`) | -| | `SimplnxCore::ComputeGroupingDensityFilter: Empty-parent edge case (-1.0f sentinel)` (Class 4 inline-invariant test) | -| **CMakeLists.txt** | `src/Plugins/SimplnxCore/test/CMakeLists.txt` line 291 | -| **Hand-review sign-off** | Michael A. Jackson (BlueQuartz Software), **2026-05-27**, recorded in the archive's inline `README.md` §"Sign-off" | - ---- +| Field | Value | +|---|---| +| **Archive** | `compute_grouping_densities_v2.tar.gz` | +| **SHA512** | `3aaabb63c4fa16f7fa192ae4ee9dbba9394ec7f1cd19aff55e399a624d495a3a778c7f6f282911f681e85cea99e4c6d15344274e9107f337af7d4a19f93784ff` | +| **Used by tests** | `SimplnxCore::ComputeGroupingDensityFilter: Exemplar A/B — all 4 configurations` | +| **Generated by** | Michael Jackson (signed off in the archive `README.md`) | +| **Generated on** | 2026-05-27 | +| **Generated at commit** | `ec22ac375` (PR #1631 merge, inferred) | -## Authorship chain +`ec22ac375` first registered the v2 archive in `src/Plugins/SimplnxCore/test/CMakeLists.txt`. The archive SHA512 matches that file. The archive README records Michael Jackson's input review and sign-off on 2026-05-27. This is a review date, not a recorded generation or execution date. -The exemplar dataset went through four distinct hands before publication. This filter is the originating use-case for the BlueQuartz LLM-attribution provenance policy; the chain is recorded explicitly because more than one LLM session touched the dataset and the boundaries between human design, LLM scaffold, and human validation matter for downstream auditors. +The archive is consumed only by the four-configuration exemplar test. The empty-parent, preflight and hidden HDF5 fixtures construct their inputs in C++. The SIMPL conversion test reads `src/Plugins/SimplnxCore/test/simpl_conversion/6_5/ComputeGroupingDensityFilter.json`. -| Step | Actor | Contribution | -|------|------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| 1 | **Claude (Opus 4.6, Anthropic)** under direction of Michael A. Jackson | Created the initial input `.dream3d` test dataset at `data/compute_grouping_density_inputs.dream3d` — the 20×5×1 `ImageGeometry` carrying the 5-feature / 2-parent hand-built microstructure (`FeatureIds`, `ParentIds` at the cell level; `Volumes`, `ParentVolumes`, `ParentIds` at the feature level; `ContiguousNL`, `NonContiguousNL` NeighborLists) designed to exercise all 4 `(UseNonContiguousNeighbors, FindCheckedFeatures)` template specializations of `FindDensityGrouping`. | -| 2 | **Michael A. Jackson** (BlueQuartz Software) | Manual structural review of the input dataset — confirmed that the `.dream3d` file contains every input array the filter consumes (`ParentIds`, `Volumes`, `ParentVolumes`, contiguous NeighborList, non-contiguous NeighborList) at the correct `DataPath`s with the correct types and shapes. Verified that the cell-level `FeatureIds` / `ParentIds` layout is consistent with the feature-level volumes (cell counts per feature: 0, 10, 20, 15, 25, 30; cell counts per parent: 0, 45, 55). Signed off as structurally correct from a filter-input-completeness point of view. | -| 3 | **Claude (Opus 4.7, Anthropic)** under direction of Michael A. Jackson | Independent test-coverage review of the same input dataset — confirmed the chosen `Volumes`, `ParentIds`, `ParentVolumes`, and neighbor-list topology are *sufficient* to exercise all 4 template specializations AND deterministically resolve the `CheckedFeatures` last-writer-wins-on-greater-volume tie-break path. Validated the Class 1 analytical hand-derivation against the dataset (see §"Canonical oracle output" below). | -| 4 | **Claude (Opus 4.7, Anthropic)** under direction of Michael A. Jackson | Translated the input dataset into a fully-reproducible Python generator script (`generate_inputs.py` in the archive) that emits the same input file as a legacy v7.0-format `.dream3d` consumable by **both** the DREAM3D 6.5.171-era `PipelineRunner` *and* SIMPLNX `nxrunner`. Also authored the `compare_outputs.py` byte-diff script. These two scripts enabled the empirical A/B comparison against the locally-rebuilt legacy `FindGroupingDensity` binary that produced the **bit-identical, zero-deviation** result documented in `vv/deviations/ComputeGroupingDensityFilter.md` and `results/ab_comparison_report.txt`. | +The supplied CI result covers the seven ordinary cases on Linux, macOS and Windows. All pass in CI at `419f9db35`. -The original hand-built fixture *design* (the choice of which configurations to cover and the `(Volumes, ParentIds, ParentVolumes, NeighborList)` tuple that achieves the coverage) was directed by Michael A. Jackson; the LLM contributions are scaffolding (initial `.dream3d` materialization, coverage cross-check, Python-script back-translation for cross-binary consumption, byte-diff comparison harness). Every numerical value was independently re-derived in the Class 1 analytical hand-derivation embedded in the V&V report. +## How it was generated ---- +The archive's `generate_inputs.py` constructs a synthetic 20×5×1 image with five populated features and two populated parents, plus index-zero placeholders. It writes one legacy-format input file for both implementations. Four JSON pipelines run `FindGroupingDensity` in a local proof version; one SIMPLNX pipeline runs all four option combinations. `compare_outputs.py` compares the resulting arrays, and the README records Michael Jackson's review of the inputs and outputs. -## How the archive was generated +All paths in the following table are members under `compute_grouping_densities_v2/` in the archive. -The published `compute_grouping_densities_v2.tar.gz` archive contains: +| Member | Purpose | +|---|---| +| `README.md` | Input review, sign-off dated 2026-05-27 and analytical expected values | +| `generate_inputs.py` | Python and h5py input generator | +| `data/compute_grouping_density_inputs.dream3d` | Shared input file with cell, feature and parent data | +| `pipelines/6_5_find_grouping_density_NC_CF.json` | Four local proof pipelines | +| `pipelines/simplnx_compute_grouping_density_ab.d3dpipeline` | SIMPLNX pipeline with all four option combinations | +| `output_legacy/6_5_find_grouping_density_NC_CF.dream3d` | Four local proof output files | +| `output_simplnx/simplnx_compute_grouping_density_ab.dream3d` | Combined SIMPLNX output file consumed by the exemplar test | +| `compare_outputs.py` | Checks array shapes, types, numerical equality and optional-array presence | +| `results/ab_comparison_report.txt` | Four matching configurations; checked-feature arrays absent on both sides when disabled | -1. **Input file** (`data/compute_grouping_density_inputs.dream3d`) — the 20×5×1 `ImageGeometry` designed in Step 1 of the authorship chain. Legacy v7.0 format (FileVersion 7.0) so the *same* file is consumed by both the locally-rebuilt legacy DREAM3D binary and SIMPLNX. Contents: - - `CellData/{FeatureIds, ParentIds}` — 100-voxel layout where each feature occupies a contiguous slab matching its volume (10 voxels for F1, 20 for F2, 15 for F3, 25 for F4, 30 for F5) - - `FeatureData`: `Volumes = [0, 10, 20, 15, 25, 30]`, `ParentIds = [0, 1, 1, 1, 2, 2]`, plus the two `NeighborList` arrays: - - `ContiguousNL = [[], [2], [1,3], [2,4], [3,5], [4]]` - - `NonContiguousNL = [[], [4], [5], [], [1], [2]]` - - `ParentData`: `ParentVolumes = [0, 45, 55]` +The synthetic input is defined as follows. Paths are relative to `DataContainer`. -2. **Generator script** (`generate_inputs.py`) — Python+h5py reproducer for the input file. The script docstring documents every SIMPL HDF5 attribute (`@FileVersion`, `@TupleDimensions`, `@ComponentDimensions`, `@ObjectType`, `@DataArrayVersion`, `@Tuple Axis Dimensions`) and the `NeighborList` two-dataset convention (main 1D array + `_NumNeighbors` companion). Any auditor can rebuild the input file byte-for-byte from source. +| Input | Values | +|---|---| +| `FeatureData/Volumes` | `[0, 10, 20, 15, 25, 30]` | +| `FeatureData/ParentIds` | `[0, 1, 1, 1, 2, 2]` | +| `FeatureData/ContiguousNeighborList` | `[[], [2], [1,3], [2,4], [3,5], [4]]` | +| `FeatureData/NonContiguousNeighborList` | `[[], [4], [5], [], [1], [2]]` | +| `ParentData/ParentVolumes` | `[0, 45, 55]` | -3. **SIMPLNX outputs** (`output_simplnx/simplnx_compute_grouping_density_ab.dream3d`) — single output file with all 4 template-specialization outputs as separate arrays: - - `/DataStructure/DataContainer/ParentData/GroupingDensities_NC_CF` (all 4 configs) - - `/DataStructure/DataContainer/FeatureData/CheckedFeatures_NC_CF` (2 configs with CF=1) +The image has 100 cells. The feature cell counts equal the supplied volumes. The parent cell counts are 45 and 55. The algorithm consumes feature and parent arrays; image coordinates and origin do not enter the density calculation. Different feature volumes and parent volumes distinguish the accumulation and checked-feature selection paths. -4. **Legacy outputs** (`output_legacy/6_5_find_grouping_density_NC_CF.dream3d`) — 4 files, one per `(NC, CF)` configuration, produced by the locally-rebuilt `FindGroupingDensity` binary — a local build of the legacy DREAM3D 6.5 source with the `tuks188/DREAM3D` `feature/770_Grouping_Density` sources pulled in. Output arrays are prefixed `6_5_` so they can be co-loaded with the SIMPLNX outputs in DREAM3D-NX for side-by-side visual diff. +The DREAM.3D v6.5.171 source tree contains no `FindGroupingDensity` filter. Its Statistics filter registration in `Source/Plugins/Statistics/StatisticsFilters/SourceList.cmake` also omits it. The archive README identifies `feature/770_Grouping_Density` as the local proof source branch. `src/Plugins/SimplnxCore/src/SimplnxCore/SimplnxCoreLegacyUUIDMapping.hpp` maps that filter's UUID `708be082-8b08-4db2-94be-52781ed4d53d` to this SIMPLNX filter. The initial SIMPLNX implementation was added by PR #1548 (`30c9b1090`). -5. **Pipelines** (`pipelines/`) — `6_5_find_grouping_density_NC_CF.json` (4 SIMPL JSON pipelines, one per config) + `simplnx_compute_grouping_density_ab.d3dpipeline` (1 SIMPLNX pipeline running all 4 configs sequentially against the same input). +The archive comparison is signed off on 2026-05-27. Its result file reports exact numerical agreement in all four configurations. The script uses `numpy.array_equal` on typed arrays, not a comparison of raw bytes. It also checks that `CheckedFeatures` is absent when disabled. This is evidence for the archived local proof comparison only; it is not a stock DREAM.3D v6.5.171 comparison. -6. **Comparison script + report** (`compare_outputs.py` + `results/ab_comparison_report.txt`) — Python+h5py reader that opens all 4 legacy outputs and the single SIMPLNX combined output and prints per-array byte-comparison results. Result is checked in for reproducibility audit. +The algorithm source changed after the sign-off date. `git log --since=2026-05-27` on `ComputeGroupingDensity.cpp` and `ComputeGroupingDensity.hpp` gives these changes: -7. **Inline `README.md`** — Michael A. Jackson's hand-review sign-off (2026-05-27) capturing the structural correctness review (Step 2), the Class 1 analytical expected-output table, the reproduction instructions, and the headline result (all 4 configurations bit-identical, zero deviations). +| Commit | Date | Algorithm-source change | +|---|---|---| +| `e198c1e2a` (PR #1626) | 2026-06-02 | Changes progress percentage formatting. | +| `ec22ac375` (PR #1631) | 2026-06-10 | Replaces copied neighbor lists with `getListSize()` and `getValue()` access, adds a failed-read return, and revises comments. | +| `a33c370ca` (PR #1697) | 2026-09-25 | Replaces message helpers with `ThrottledMessageHandler`. | ---- +These changes are why the report does not claim current runtime parity from the archived comparison. ## Canonical oracle output -| DataPath | Source of expected values | -|---------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `/DataContainer/ParentData/GroupingDensities_NC0_CF0` | **Class 1 analytical** — `Parent1 = 45/70 ≈ 0.6428571`, `Parent2 = 55/70 ≈ 0.7857143` (touched-set = assigned ∪ contiguous-NL neighbors of assigned) | -| `/DataContainer/ParentData/GroupingDensities_NC0_CF1` | **Class 1 analytical** — identical density values to `NC0_CF0` | -| `/DataContainer/ParentData/GroupingDensities_NC1_CF0` | **Class 1 analytical** — `Parent1 = 45/100 = 0.45`, `Parent2 = 55/100 = 0.55` (touched-set = assigned ∪ contiguous-NL ∪ non-contiguous-NL neighbors of assigned; both parents touch all 5 features) | -| `/DataContainer/ParentData/GroupingDensities_NC1_CF1` | **Class 1 analytical** — identical density values to `NC1_CF0` | -| `/DataContainer/FeatureData/CheckedFeatures_NC0_CF1` | **Class 1 analytical** — `[0, 1, 1, 2, 2, 2]` (parent 2 wins the tie at features 3 & 4 because `ParentVolumes[2] = 55 > ParentVolumes[1] = 45` and the algorithm uses last-writer-wins-on-greater-volume semantics) | -| `/DataContainer/FeatureData/CheckedFeatures_NC1_CF1` | **Class 1 analytical** — `[0, 2, 2, 2, 2, 2]` (both parents touch every feature; parent 2 wins on every feature for the same volume-tie reason) | -| All `GroupingDensities_*` arrays (invariant predicates) | **Class 4 invariant** — `density[0] == 0.0f`; for `i ≥ 1`, `density[i] > 0 ∨ == -1.0f`; for `i ≥ 1`, `density[i] ≤ 1.0f`. Asserted inline in the test for every fixture. | -| `CheckedFeatures_*` arrays (invariant predicates) | **Class 4 invariant** — `CheckedFeatures[k] ∈ {0, …, numParents-1}`; `CheckedFeatures[0] == 0`. Asserted inline in the test for the two `_CF1` fixtures. | +For each parent, form the union of its assigned features and their contiguous neighbors. Include non-contiguous neighbors when selected. Sum each feature volume once. Divide the parent volume by that sum; use `-1.0f` when the sum is zero. -Bit-exact float32 values produced by both binaries (from `ab_comparison_report.txt`): -- `0.6428571343421936` (45/70), `0.7857142686843872` (55/70) -- `0.44999998807907104` (0.45), `0.550000011920929` (0.55) +For the contiguous-only input, parent 1 touches `{1,2,3,4}` and parent 2 touches `{3,4,5}`. Both sums equal 70. With non-contiguous neighbors enabled, each parent touches `{1,2,3,4,5}` and the sum equals 100. -The unit test uses `UnitTest::CompareDataArrays` and `` to verify bit-exact equality between the SIMPLNX-generated arrays and the archived exemplar arrays. Class 4 invariant predicates are asserted inline in the same test, independent of the exemplar. +The following DataPaths refer to arrays in the combined SIMPLNX output. The HDF5 file contains them below `/DataStructure`. -Full Class 1 hand-derivation table is embedded in `src/Plugins/SimplnxCore/vv/ComputeGroupingDensityFilter.md` §"Oracle". +| DataPath | Source of expected values | +|---|---| +| `/DataContainer/ParentData/GroupingDensities_NC0_CF0` | Class 1: `[0, 45/70, 55/70]` | +| `/DataContainer/ParentData/GroupingDensities_NC0_CF1` | Class 1: `[0, 45/70, 55/70]` | +| `/DataContainer/ParentData/GroupingDensities_NC1_CF0` | Class 1: `[0, 45/100, 55/100]` | +| `/DataContainer/ParentData/GroupingDensities_NC1_CF1` | Class 1: `[0, 45/100, 55/100]` | +| `/DataContainer/FeatureData/CheckedFeatures_NC0_CF1` | Class 1: `[0, 1, 1, 2, 2, 2]` | +| `/DataContainer/FeatureData/CheckedFeatures_NC1_CF1` | Class 1: `[0, 2, 2, 2, 2, 2]` | ---- +The result file records float32 densities `0.6428571343421936`, `0.7857142686843872`, `0.44999998807907104` and `0.550000011920929`. These equal the float32 values of the four ratios above. -## Oracle provenance (Classes 2, 3, 5 only) +Checked-feature selection uses a strict `>` comparison against an initial claimed volume of zero. Parent 2 has volume 55 and replaces parent 1, whose volume is 45, on shared features. Equal positive parent volumes keep the first claimant; the fixture does not test equal volumes. A zero-volume parent does not replace an initial zero claim. -N/A — Class 1 and Class 4 oracles only. No reference-library invocation, no paper-figure reproduction, no expert-visual sign-off needed. +`test/ComputeGroupingDensityTest.cpp` compares all generated densities with the archive and compares checked-feature IDs when enabled. `CompareDataArrays` in `test/UnitTestCommon/include/simplnx/UnitTest/UnitTestCommon.hpp` accepts absolute differences up to `0.0001`; integer comparison is exact. The exemplar test is therefore not a bitwise floating-point test. ---- +The same test checks Class 4 properties: parent-zero density is zero; each other density is positive or the sentinel and at most one; checked-feature IDs are in range; feature zero is unclaimed. The upper density bound assumes that parent volumes agree with assigned feature volumes, as they do in this fixture. The separate empty-parent test supplies feature volumes 5 and 10 to parent 1 and no features to parent 2. It checks density 1 within relative tolerance `0.0001` and sentinel `-1.0f` exactly. -## Second-engineer oracle review +The hidden HDF5 fixture encodes an additional Class 1 construction in C++. Features 2 and 3 have volumes 4 and 8 and belong to parent 1, whose volume is 12. Shared neighbor 4 adds volume 4 once, giving density `12/16 = 0.75`. Optional neighbor 8 adds volume 8, giving `12/24 = 0.5`. Feature indices 2 and 3 cross a three-tuple list boundary. Parent 2 stays at `-1.0f`, and the fixture checks the expected claims or absence of the disabled checked-feature array. -- **Reviewer:** *Skipped* -- **Date:** N/A -- **Skip reason:** The Class 1 derivation is set-union sums + ratio division on a 5-feature hand-built fixture (high-school arithmetic). External cross-validation was obtained via the independently-authored legacy `FindGroupingDensity` implementation (`tuks188/DREAM3D` `feature/770_Grouping_Density`, rebuilt locally against the legacy DREAM3D 6.5 source): the A/B comparison produced **bit-identical** agreement across all 4 `(NC, CF)` configurations and both output arrays (`results/ab_comparison_report.txt`). Any oracle-derivation error would have surfaced as a deviation. Formal second-engineer review of a 5-feature analytical oracle was not justified given this cross-check. +## Oracle provenance (Classes 2, 3, 5 only) ---- +Not applicable. The correctness oracles are Classes 1 and 4. The local proof outputs provide comparison evidence; they do not define the volume-ratio formula. -## Regenerated to fix a circular-oracle situation? +## Second-engineer oracle review -**Yes — v2 supersedes v1 to remove a circular-oracle concern.** +- **Reviewer:** skipped +- **Date:** Not applicable +- **Skip reason:** The recorded rationale uses the five-feature set-union and ratio derivation, with the local proof comparison as an external cross-check. -| Field | v1 (retired) | -|-----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Archive name | `compute_grouping_densities.tar.gz` | -| Provenance | Generated from a single SIMPLNX `ComputeGroupingDensityFilter` run by the filter author (PR #1548), without an independent oracle and without a legacy-binary cross-check. The exemplar arrays in v1 were therefore *whatever SIMPLNX produced at the time the archive was published* — a textbook circular oracle (the test compares SIMPLNX-now against SIMPLNX-then; an algorithmic regression that simultaneously breaks the live code AND the regeneration script would slip through). | -| Retired on | 2026-05-27 (V&V cycle close) | -| CMakeLists.txt | `download_test_data(... compute_grouping_densities.tar.gz ...)` removed from this filter's section of `src/Plugins/SimplnxCore/test/CMakeLists.txt` (no other test in this filter family consumed it). | +The archive README names Michael Jackson as the input reviewer on 2026-05-27. The report sign-off has no named second engineer. -v2 closes the circular-oracle gap by: +## Regenerated to fix a circular-oracle situation? -1. **Independent oracle** — the Class 1 hand-derivation embedded in the V&V report was carried out from the input definition alone, with no reference to either DREAM3D 6.5.171 or SIMPLNX output. -2. **External cross-validation** — the same input was fed through a locally-rebuilt legacy `FindGroupingDensity` binary; output was bit-identical to SIMPLNX across all 4 configurations (see `results/ab_comparison_report.txt`). -3. **Reproducibility** — `generate_inputs.py` and `compare_outputs.py` let any auditor regenerate both the input and the comparison from source. -4. **Coverage** — v1 carried output for only one `(NC, CF)` configuration; v2 carries all four, exercising every template specialization of `FindDensityGrouping`. +Yes. PR #1631 (`ec22ac375`) replaces `compute_grouping_densities.tar.gz` with `compute_grouping_densities_v2.tar.gz` and replaces the old archive consumer and four redundant execution cases with the four-configuration test. The v2 README supplies explicit hand-calculated expectations, and its result file supplies the local proof comparison. The independent mathematical definition is the set-union volume ratio above. -v2 ships with the inline `README.md` sign-off so the provenance travels with the archive even when consumed outside the source tree. +PR #1548 (`30c9b1090`) first registered the v1 archive. The current CMake file registers only v2 for this filter. The v2 archive includes its generator, comparison script, pipelines and signed README so the derivation and comparison can be inspected with the data. diff --git a/src/Plugins/SimplnxCore/vv/provenance/CreateFeatureArrayFromElementArrayFilter.md b/src/Plugins/SimplnxCore/vv/provenance/CreateFeatureArrayFromElementArrayFilter.md index b1dc2d8924..8be688695f 100644 --- a/src/Plugins/SimplnxCore/vv/provenance/CreateFeatureArrayFromElementArrayFilter.md +++ b/src/Plugins/SimplnxCore/vv/provenance/CreateFeatureArrayFromElementArrayFilter.md @@ -1,88 +1,92 @@ -# Exemplar Archive Provenance: CreateFeatureArrayFromElementArrayFilter +# Exemplar Archive Provenance: 6_5_test_data_1_v2.tar.gz -This sidecar documents the oracle and archive strategy for `CreateFeatureArrayFromElementArrayFilter`. It covers both the Class 1 analytical oracle (inline, no archive needed) and the shared input archive used by the existing regression tests. +This sidecar records the shared regression archive, independent analytical fixtures, release-source evidence and saved legacy comparison for `CreateFeatureArrayFromElementArrayFilter`. ---- - -## Oracle identity +## Archive identity | Field | Value | |---|---| -| **Oracle class** | Class 1 (Analytical) primary + Class 4 (Invariant) companion | -| **Oracle archive** | *None — Class 1 and Class 4 oracles require no exemplar archive; expected values are encoded as inline `REQUIRE` assertions in test source* | -| **A/B comparison artifacts** | `OneDrive - feature_from_element_vv/` — `generate_input.py`, `pipeline_simpl_{1,3}comp.json`, `pipeline_nx_{1,3}comp.d3dpipeline`, `compare_outputs.py`, `run_all.sh` | -| **A/B run date** | 2026-07-23 | -| **A/B result** | PASS — bit-identical on 1-component float32 and 3-component uint8 fixtures | -| **Regression input archive** | `6_5_test_data_1_v2.tar.gz` (shared across multiple SimplnxCore tests; not an oracle artifact for this filter) | -| **Regression input SHA512** | `585b51ba1da9784a204fe88073ca562b45afd7007cf451b0193079b885c4b4caff7cf21b13e016433b84155546ac0f73f003a8b8ebb1c58360b2c56de3027d6c` | -| **Used by tests** | `Valid filter execution - 1 Component`, `Valid filter execution - 3 Component` (regression); `AnalyticalFixtures — AF-1` through `AF-9` (V&V oracle, no archive) | -| **Generated by** | BlueQuartzSoftware (shared Small IN100 dataset; not generated for this filter specifically) | -| **Generated on** | Prior to simplnx V&V initiative (provenance of the `6_5_test_data_1_v2.tar.gz` archive is established by other filters' V&V records) | +| **Archive** | `6_5_test_data_1_v2.tar.gz` | +| **SHA512** | `585b51ba1da9784a204fe88073ca562b45afd7007cf451b0193079b885c4b4caff7cf21b13e016433b84155546ac0f73f003a8b8ebb1c58360b2c56de3027d6c` | +| **Used by tests** | `SimplnxCore::CreateFeatureArrayFromElementArrayFilter: Valid filter execution - 1 Component`; `SimplnxCore::CreateFeatureArrayFromElementArrayFilter: Valid filter execution - 3 Component` | +| **Generated by** | Joey Kleingers (inferred from PR #1021) | +| **Generated on** | 2024-07-17 to 2024-07-18 (inferred from PR #1021) | +| **Generated at commit** | `c3be139e2` (PR #1021 merge, inferred) | ---- +`src/Plugins/SimplnxCore/test/CMakeLists.txt` registers this name and SHA512. The first registration is in PR #1021. All pass in CI at `419f9db35` on Linux, macOS and Windows in in-core builds. -## How the oracle was established +## How it was generated -The `CreateFeatureArrayFromElementArrayFilter` implements a pure indirection copy: `output[featureId * C + comp] = input[cellIdx * C + comp]`, with last-writer-wins when multiple cells share a `featureId`. This operation has a trivial closed-form expected output for any hand-constructed input, making Class 1 (Analytical) the natural and lowest-drift oracle choice. +The shared archive contains `6_5_test_data_1_v2.dream3d`, which the tests load as Small IN100. The two tests read element arrays and compare the generated feature arrays with the stored ConfidenceIndex and IPFColors arrays. Separate hand-constructed C++ inputs supply the independent oracle; these fixtures do not use archived expected values. -Nine analytical fixtures were designed and hand-derived without executing any DREAM3D implementation. The three that carry the Class 1 output oracle are: +The independent fixtures are defined in `src/Plugins/SimplnxCore/test/CreateFeatureArrayFromElementArrayTest.cpp`. AF-1 through AF-9 use small arrays with explicit FeatureIds. `Bulk last-value boundary oracle` uses 65,538 element tuples, three unequal components, a feature first seen in the final tuple, and a gap feature. This filter does not use geometry, so axis dimensions and origins do not enter its oracle. -- **AF-1** (single-component, consistent): 5 cells, `featureIds = [0,1,2,1,2]`, `values = [5.0, 10.0, 20.0, 10.0, 20.0]` → expected output `[5.0, 10.0, 20.0]`, 0 warnings. -- **AF-2** (single-component, inconsistent): 4 cells, `featureIds = [1,2,1,2]`, `values = [10.0, 20.0, 15.0, 20.0]` → expected output `[0.0, 15.0, 20.0]`, 1 warning (feature 1: first=10.0, last=15.0). -- **AF-3** (3-component, consistent): 4 cells, `featureIds = [1,2,1,2]`, uint8 3-comp values `[[10,20,30],[40,50,60],[10,20,30],[40,50,60]]` → expected output `[[0,0,0],[10,20,30],[40,50,60]]`, 0 warnings. +**Saved legacy comparison** -The AF-1/AF-2/AF-3 derivations follow directly from the per-cell copy loop at `Algorithms/CreateFeatureArrayFromElementArray.cpp` lines 25–56 and are embedded as comments in the test cases. AF-6 adds a Class 1 + Class 4 resize-gap case (`featureIds = [0, 2, 0, 2]`, feature 1 never mapped → `output[1]` retains its init value). +Run against DREAM.3D v6.5.171 on 2026-07-23: 3 of 3 output-array cases match. Each array is bit-identical between the two applications and equals its hand-derived expectation. The inconsistent-value case emits one `-1000` warning in each application. The separate oversized-matrix case gives legacy error `-5556`; the SIMPLNX version used for that run truncates the matrix from ten tuples to five. -The remaining five are Class 4 error-path and boundary fixtures: +The run uses stock DREAM.3D v6.5.171. `vv_work:CreateFeatureArrayFromElementArrayFilter/notes/LegacyComparison.md` identifies the official application, and `vv_work:CreateFeatureArrayFromElementArrayFilter/results/legacy_toy_run.log` records `SIMPLib Version 1.2.828.f45085c83`. The fixture is a 2×2×2 image with eight cells, defined by `vv_work:CreateFeatureArrayFromElementArrayFilter/data/make_input.py`. -- **AF-4** — all-negative `featureIds` → the `minValue < 0` guard (error -81880). -- **AF-5** — shrink-protection loop, by creating an AM child array with more tuples than the resize target (error -81881). -- **AF-7** — `featureIds` array with 0 tuples → the empty-array guard (error -81882). -- **AF-8** — mixed negative and positive `featureIds`, which the pre-fix `maxValue < 0` guard missed and the `minValue < 0` guard catches (error -81880). -- **AF-9** — cell array and `featureIds` tuple counts differ → the `preflightImpl` tuple-count check (error -81883). +| Output | Hand-derived expectation | +|---|---| +| `DataContainer/FeatureAM_A/Copied_A` | int32 scalar: `[5,10,0,30,40]` | +| `DataContainer/FeatureAM_A/Copied_A3` | float32 triples: `[[0.25,0.5,0.75],[1.25,1.5,1.75],[0,0,0],[3.25,3.5,3.75],[4.25,4.5,4.75]]` | +| `DataContainer/FeatureAM_C/Copied_B` | int32 scalar: `[0,11,21]` | ---- +The comparison script and results are `vv_work:CreateFeatureArrayFromElementArrayFilter/results/compare_ab.py` and `vv_work:CreateFeatureArrayFromElementArrayFilter/results/compare_ab_output.txt`. The corresponding inputs are `vv_work:CreateFeatureArrayFromElementArrayFilter/pipelines/legacy_CreateFeatureArray_toy.json` and `vv_work:CreateFeatureArrayFromElementArrayFilter/pipelines/nx_CreateFeatureArray_toy.d3dpipeline`. The sizing evidence is in `vv_work:CreateFeatureArrayFromElementArrayFilter/results/legacy_oversized_run.log` and `vv_work:CreateFeatureArrayFromElementArrayFilter/notes/LegacyComparison.md`. -## Canonical oracle output +Two algorithm changes follow the run: `fa31947b5` (2026-08-07, PR #1695) adds validation and growth-only sizing; `4ec21e93f` (2026-09-20, PR #1568) replaces scalar access with chunked reads and a final buffered write. Thus the saved comparison is evidence for its dated execution, not for execution of the current algorithm. In particular, current SIMPLNX preserves an oversized feature matrix. -The oracle output is encoded entirely as inline `REQUIRE` assertions — there is no oracle archive and no external script. +**Release-source evidence** -| Oracle content | Source of expected values | -|---|---| -| `output[0] = 5.0` (AF-1, feature 0) | Class 1 hand derivation: only cell 0 maps to feature 0 | -| `output[1] = 10.0` (AF-1, feature 1) | Class 1: cells 1 and 3 both write 10.0 (consistent; last = first) | -| `output[2] = 20.0` (AF-1, feature 2) | Class 1: cells 2 and 4 both write 20.0 (consistent) | -| `output[0] = 0.0` (AF-2, feature 0) | Class 4 invariant: never written; CreateArrayAction fill="0" | -| `output[1] = 15.0` (AF-2, feature 1) | Class 1: last-writer cell 2 writes 15.0 (overrides 10.0) | -| `output[2] = 20.0` (AF-2, feature 2) | Class 1: cells 1 and 3 write 20.0 (consistent) | -| warning count == 1 (AF-2) | Class 4 invariant: `result.warnings().empty()` guard → exactly one warning | -| `output[1] = [10,20,30]` (AF-3) | Class 1: both cells mapping to feature 1 have value [10,20,30] | -| `output[2] = [40,50,60]` (AF-3) | Class 1: both cells mapping to feature 2 have value [40,50,60] | -| Output AM shape = `{maxFeatureId + 1}` (all fixtures) | Class 4 invariant: `cellFeatureAttrMat.resizeTuples({maxValue + 1})` at line 109 | +The legacy source is `Source/SIMPLib/CoreFilters/CreateFeatureArrayFromElementArray.cpp` in SIMPL at `0afaa6854ac0ef27df17278e11be711f74f77b90`. This is the last SIMPL commit on or before the DREAM.3D `v6.5.171` tag date, 2026-05-27T16:41:12-04:00. Its `execute()` rejects undersized and oversized feature matrices with `-5555` and `-5556`. It does not reject negative IDs or unequal input tuple counts. It has neither the empty-array maximum lookup nor the unconditional feature-matrix resizing that caused the SIMPLNX-only defects. ---- +Each release source was inspected with `git show upstream/release/v:` for v7.0.0, v7.0.1, v7.0.2, v7.0.3, v7.2.0, v7.2.1, v7.2.2, v7.4.0, v7.4.1 and v7.4.2. Source paths below are relative to `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/`. -## Oracle provenance +| Releases | Source | D2: negative IDs | D3: unequal tuples | D4: empty IDs | D5: truncation | +|---|---|---|---|---|---| +| DREAM3D-NX v7.0.0 through v7.4.0 | `CreateFeatureArrayFromElementArrayFilter.cpp` | No rejection | No length check | Maximum lookup indexes an empty store | Unconditional resize | +| DREAM3D-NX v7.4.1 | `CreateFeatureArrayFromElementArrayFilter.cpp`; `Algorithms/CreateFeatureArrayFromElementArray.cpp` | No rejection | No length check | Maximum lookup indexes an empty store | Unconditional resize | +| DREAM3D-NX v7.4.2 | Same paths | Error `-81880` | Error `-81883` | Error `-81882` | Growth-only resize and error `-81881` | -Classes 1 and 4 require no external provenance block. The expected values are derivable by arithmetic from the input definition. The derivations are embedded as inline comments in the `AnalyticalFixtures` test functions. +PR #1695 (`fa31947b5`) supplies all four corrections. DREAM.3D v6.5.171 was affected by D2 and D3; it was not affected by D4 and D5. Current chunked execution retains the four protections. D1 keeps its original ID spelling, `CreateFeatureArrayFromElementArray-D1`; D2 through D5 use `CreateFeatureArrayFromElementArrayFilter-D`. -*No Class 2, 3, or 5 oracle was used for this filter.* +## Canonical oracle output ---- +The archive arrays supply regression expectations. Independent canonical values are encoded in the C++ fixtures. -## Circular-oracle note on existing regression tests +| DataPath | Source of expected values | +|---|---| +| `/DataContainer/CellFeatureData/ConfidenceIndex` | Stored Small IN100 regression array; used by the one-component archive test. | +| `/DataContainer/CellFeatureData/IPFColors` | Stored Small IN100 regression array; used by the three-component archive test. | +| `/ParentGroup/FeatureData/OutputFeatureArray` | Class 1 hand derivation in AF-1, AF-2, AF-3, AF-6 and `Bulk last-value boundary oracle`. | -The existing `Valid filter execution - 1 Component` and `Valid filter execution - 3 Component` tests compare SIMPLNX output against `CellFeatureData/ConfidenceIndex` and `CellFeatureData/IPFColors` arrays pre-existing in `6_5_test_data_1_v2.dream3d`. These feature-level arrays were generated by DREAM3D 6.5.x running the equivalent `CreateFeatureArrayFromElementArray` filter on the same cell data — making them **legacy output, not an independent oracle**. Per policy, "legacy DREAM3D 6.5.171 produced this output" is not a valid correctness oracle. +| Fixture | Expected output or error | +|---|---| +| AF-1 | float32 `[5,10,20]`; no warnings. | +| AF-2 | float32 `[0,15,20]`; exactly one warning. | +| AF-3 | uint8 `[[0,0,0],[10,20,30],[40,50,60]]`; no warnings. | +| AF-4 | All-negative IDs → `-81880`. | +| AF-5 | Growing a two-tuple matrix to four would truncate its five-tuple child → `-81881`. | +| AF-6 | float32 `[5,0,30]`; no warnings; the unused feature stays zero. | +| AF-7 | Empty IDs → `-81882`. | +| AF-8 | Mixed negative/nonnegative IDs → `-81880`. | +| AF-9 | Four element tuples and two ID tuples → preflight error `-81883`. | +| Bulk last-value boundary oracle | int32 `[[0,0,0],[7,70,700],[0,0,0],[2,20,200]]`; exactly one warning `-1000`. | -The circular nature of these tests is mitigated for this specific filter because the algorithm is a pure copy with deterministic, analytically verifiable output. The AnalyticalFixtures tests (AF-1, AF-2, AF-3) provide the independent oracle; the existing regression tests are retained as integration coverage to catch regressions against known baseline behavior. +For each mapped feature, its last element tuple supplies the expected values. Unused output tuples are zero. AF-1, AF-2, AF-3 and AF-6 assert output tuple count, data type and component count. The bulk fixture asserts tuple count, component count, all 12 values and the warning code. Its three changed components check suppression of additional warnings. No successful fixture tests a matrix that already has enough tuples; the archive tests also start with a one-tuple matrix. -*No archive regeneration is required.* The existing regression tests use the shared input archive as input data only; the oracle output lives in the test source as inline assertions. +## Oracle provenance (Classes 2, 3, 5 only) ---- +Not applicable. The independent oracles are Classes 1 and 4 and are encoded in the test source. ## Second-engineer oracle review -- **Reviewer:** Michael Jackson <mike.jackson@bluequartz.net> -- **Review date:** 2026-08-07 (at PR #1695 review) -- **Scope:** The Class 1 hand derivations for AF-1/AF-2/AF-3/AF-6 were checked against the closed form `output[featureId * C + comp] = input[lastCellIdxForFeature * C + comp]` at lines 25–56 of `Algorithms/CreateFeatureArrayFromElementArray.cpp`, and the Class 4 error-path oracles (AF-4/AF-5/AF-7/AF-8/AF-9) were checked against the guards they claim to exercise: -81880 (`minValue < 0`), -81881 (shrink protection), -81882 (empty featureIds), and -81883 (preflight tuple-count mismatch in `CreateFeatureArrayFromElementArrayFilter::preflightImpl`). -- **Result:** No oracle-design defects found. The derivations are independent of the SIMPLNX implementation, and the fixture set covers the consistent, inconsistent, multi-component, resize-gap, and error-boundary cases. Sign-off granted. +- **Reviewer:** Michael Jackson +- **Date:** 2026-08-07 + +The report records review of AF-1 through AF-9 in PR #1695. The bulk-boundary case was added later in PR #1568. + +## Regenerated to fix a circular-oracle situation? + +The V&V added independent analytical fixtures without replacing the shared archive. The stored ConfidenceIndex and IPFColors arrays remain regression expectations. They do not define the Class 1 oracle. diff --git a/src/Plugins/SimplnxCore/vv/provenance/FillBadDataFilter.md b/src/Plugins/SimplnxCore/vv/provenance/FillBadDataFilter.md index 7c875eaa71..9fd2cf3373 100644 --- a/src/Plugins/SimplnxCore/vv/provenance/FillBadDataFilter.md +++ b/src/Plugins/SimplnxCore/vv/provenance/FillBadDataFilter.md @@ -1,8 +1,6 @@ # Exemplar Archive Provenance: 6_5_fill_bad_data.tar.gz -This sidecar records the origin of the exemplar archive used in `FillBadDataFilter` unit tests. The archive name and SHA512 must match `download_test_data()` in `src/Plugins/SimplnxCore/test/CMakeLists.txt`. - ---- +This record covers the analytical archive, comparison artifacts and source evidence for `FillBadDataFilter`. ## Archive identity @@ -10,49 +8,61 @@ This sidecar records the origin of the exemplar archive used in `FillBadDataFilt |---|---| | **Archive** | `6_5_fill_bad_data.tar.gz` | | **SHA512** | `73b6835252135619325d9b02b48286ca95f0235820610bddcc43ec0a3f075337481bace00bf20e9d59744f1d4024cc18be624ec522922a6461b55ae8dbb0e628` | -| **Used by tests** | `FillBadData_SmallIN100`, `Test01` through `Test07`, `Test11`, `Test13` (10 fixture-based tests) | -| **Generated by** | ** | -| **Generated on** | ** | +| **Used by tests** | `FillBadData_SmallIN100`, `Test01` through `Test07`, `Test11` and `Test13` (10 cases). | +| **Generated by** | Michael Jackson (inferred from PR #1515) | +| **Generated on** | 2026-01-28 to 2026-02-05 (inferred from PR #1515) | +| **Generated at commit** | `e17f0d7dc` (PR #1515 merge, inferred) | -## Archive contents and oracle classification +The archive name first appears in `src/Plugins/SimplnxCore/test/CMakeLists.txt` in PR #1515. The registered SHA512 matches the report and this identity table. All pass in CI at `419f9db35` on Linux, macOS and Windows (in-core builds). -The archive contains two groups with different oracle classifications: +## How it was generated -**Group 1 — Legacy-sourced (Class 2 oracle):** -`6_5_input.dream3d` and `6_5_exemplar.dream3d`. The exemplar was generated by running a DREAM3D 6.5.x pipeline on the Small IN100 dataset with `FillBadData` (`MinAllowedDefectSize=1000`, `StoreAsNewPhase=false`). Serves as the oracle for `FillBadData_SmallIN100`. +The archive contains nine synthetic fixture pairs: Tests 01–07, 11 and 13. Their `test_NN_description.txt` files derive the expected FeatureIds and Phases from the selected component sizes and neighbor votes. `generate_dream3d_files.py` reads the input and expected text arrays, creates the geometry and arrays, and writes each file; it does not execute `FillBadDataFilter`. The Small IN100 pair, `6_5_input.dream3d` and `6_5_exemplar.dream3d`, supplies a separate regression comparison. -**Group 2 — Synthetic fixtures (Class 1 oracle — analytically derived):** -`test_NN_input.dream3d` and `test_NN_expected.dream3d` pairs for Tests 01–07, 11, and 13. Expected values originate from hand-authored `test_NN_expected_featureids.txt` files (and `test_NN_expected_phases.txt` for Test 13) derived analytically from the algorithm specification. `generate_dream3d_files.py` serializes these text arrays into HDF5 using `CreateGeometryFilter` + `CreateDataArrayFilter` + `WriteDREAM3DFilter`; it does not run `FillBadDataFilter`. +The manual comparison record, `vv_work:FillBadDataFilter/comparison_report.md`, is dated 2026-07-03. It records one 7×7×3 case at threshold 10 with Store Defects as New Phase off. It reports exact agreement of all 147 FeatureIds between the runner identified as DREAM.3D v6.5.171, a local proof version, SIMPLNX and the analytical array. The one-cell region becomes FeatureId 2; the 14-cell region stays zero. Output counts are 14 zeros, 124 ones and 9 twos. -## Oracle provenance detail +`vv_work:FillBadDataFilter/create_test08_legacy.py` constructs the shared input. `vv_work:FillBadDataFilter/compare_outputs.py` contains the literal expected array and element-wise comparison. The preserved pipelines are `vv_work:FillBadDataFilter/fill_bad_data_6_5.json` and `vv_work:FillBadDataFilter/fill_bad_data_nx.d3dpipeline`. This comparison predates changes to the algorithm and does not verify current output parity. -### Class 1 — Analytically derived (Tests 01–07, 11, 13) +The algorithm files changed after the comparison in `a9d7e17e3` (2026-07-14, V&V fixes), `4ec21e93f` (2026-09-20, BFS/CCL storage rewrite), `a33c370ca` (2026-09-25, messaging API) and `62000012e` (2026-10-20, progress reporting). These entries come from `git log --since=2026-07-03` on the current algorithm files. -- **Method:** Each `test_NN_description.txt` traces the algorithm phase-by-phase against the given input and parameters, deriving which components form, how they are classified against the threshold, and the required fill result. -- **Serialization:** `generate_dream3d_files.py` reads hand-authored arrays and writes them to HDF5. It does not execute `FillBadDataFilter`. -- **Evidence of independence:** Test 11 explicitly traces neighbor scan order (-Z, -Y, -X, +X, +Y, +Z), identifies Feature 1 reaching count 3 at the -X check before Feature 2 does, and specifies Feature 1 as the expected result — reasoning that cannot originate from a simulation output. +Release exposure was checked directly in `upstream/release/v7.0.0`, `v7.0.1`, `v7.0.2`, `v7.0.3`, `v7.2.0`, `v7.2.1`, `v7.2.2`, `v7.4.0`, `v7.4.1` and `v7.4.2`. The inspected sources are `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/FillBadDataFilter.cpp`, `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/FillBadData.cpp` and `src/simplnx/Utilities/DataGroupUtilities.cpp`. -### Class 2 — Reference implementation (`6_5_exemplar.dream3d`) +| Defect | Release-source result | Correction | +|---|---|---| +| D1 | v7.0.0 through v7.4.1 discard the minimum-size error result. | PR #1653 (`a9d7e17e3`); v7.4.2 returns -16500. The old report identifier was `FillBadDataFilter-B1`. | +| D2 | v7.0.0 through v7.4.1 have no no-progress exit in the fill loop. | PR #1653; v7.4.2 warns and stops if no source was assigned. | +| D3 | v7.0.0 through v7.4.1 compare a signed FeatureId with an unsigned maximum. | PR #1653; v7.4.2 accepts only positive IDs in the maximum scan. | +| D4 | v7.0.0 through v7.4.0 narrow BFS component sizes to Int32; v7.4.1 narrows CCL component sizes. | PR #1653; v7.4.2 compares full-width CCL counts. Current BFS also uses full-width counts. | +| D5 | v7.0.0 through v7.4.1 store neighbor indices in Int32. | PR #1653; v7.4.2 uses Int64. | +| D6 | v7.0.0 through v7.4.2 select image dimensions independently of FeatureIds without a containment check. | PR #1829 (`419f9db35`) validates membership in the selected Cell Attribute Matrix. | +| D7 | v7.0.0 through v7.4.2 pass ignored paths directly to child discovery without alias normalization. | PR #1829 normalizes paths before discovery and modification reporting. | -- **Source:** DREAM3D 6.5.x `FillBadData` filter -- **Exact version:** ** -- **Runner:** DREAM3D 6.5.x `PipelineRunner` -- **Pipeline file:** Not preserved in the source tree -- **Note:** The `6_5_` prefix on both files follows this project's convention for legacy-sourced data and is consistent with the runtime A/B comparison (Test 08, 2026-07-03) confirming DREAM3D 6.5.171 and DREAM3D-NX agree. +The DREAM.3D v6.5.171 source is `Source/Plugins/Processing/ProcessingFilters/FillBadData.cpp` at tag `v6.5.171`. It has no minimum-size validation or no-progress exit, compares signed IDs with an unsigned maximum, narrows region sizes to Int32 and stores Int32 neighbor indices. It was affected by D1–D5. It derives the geometry from the FeatureIds container and has no shared-parent alias path, so it was not affected by D6 or D7. -## Oracle canonical paths +## Canonical oracle output -| DataPath | Oracle class | +| DataPath | Source of expected values | |---|---| -| `DataContainer/CellData/*` — `FillBadData_SmallIN100` | **Class 2** — DREAM3D 6.5.x `FillBadData` pipeline on Small IN100 | -| `DataContainer/CellData/*` — Tests 01–07, 11, 13 | **Class 1** — hand-authored `test_NN_expected_featureids.txt`, serialized by `generate_dream3d_files.py` | +| `/DataContainer/CellData/FeatureIds` — Tests 01–07, 11 and 13 | Class 1 text arrays in `test_NN_expected_featureids.txt`, with the derivation in each `test_NN_description.txt`. | +| `/DataContainer/CellData/Phases` — Test 13 | Class 1 `test_13_expected_phases.txt`: 50 retained defect cells have phase 2; the other 75 cells retain phase 1. | + +Test 03 retains exactly 25 cells at threshold 25. Test 11 gives each feature three neighbors; Feature 1 reaches three votes first in the order -Z, -Y, -X, +X, +Y, +Z. The converter supplies phase 1 when a phase text file is absent. The Small IN100 exemplar is a regression target, not an independent correctness oracle. Its compared cell arrays are FeatureIds, Phases, Mask and three-component EulerAngles; both archive files have 189×201×117 cells. + +The hidden `real HDF5 cross-slice defect oracle` defines separate expectations in C++: indices 37, 62 and 87 stay zero at threshold 3, and index 56 fills from a good neighbor. It asserts the full FeatureIds, Phases and two-component payload arrays for both new-phase settings. These expectations do not come from the archive. + +`AllBadData_TerminatesWithoutHang` does not exercise the no-progress exit: its nine zero cells meet threshold 1 and remain a retained component. The negative-input allocation guard and the two large-index corrections also have no direct regression fixture. + +## Oracle provenance (Classes 2, 3, 5 only) + +Not applicable. The independent correctness oracle is Class 1. Legacy output is used only for regression and migration comparisons. + +## Second-engineer oracle review -## Second-engineer review +- **Reviewer:** Michael Jackson, as named in the existing oracle review record. +- **Date:** 2026-07-16. -- **Reviewer:** Michael Jackson (technical authority) -- **Date:** 2026-07-16 -- **Review focus:** the hand-authored derivations for Test 11 (tie-breaking scan order) and Test 13 (StoreAsNewPhase phase assignment). +The report sign-off names Michael Jackson on the same date and does not identify a separate second engineer. -## Regeneration note +## Regenerated to fix a circular-oracle situation? -No regeneration was required. The `test_NN_expected.dream3d` files were never circular oracles — `generate_dream3d_files.py` is a format converter that does not run `FillBadDataFilter`. The original archive is retained unchanged. +The nine analytical fixtures use text expectations serialized without executing FillBadData. Their expected outputs do not depend on filter-generated output. The archive remains unchanged. diff --git a/src/Plugins/SimplnxCore/vv/provenance/IdentifySampleFilter.md b/src/Plugins/SimplnxCore/vv/provenance/IdentifySampleFilter.md index e12214130c..6eaa3f3a21 100644 --- a/src/Plugins/SimplnxCore/vv/provenance/IdentifySampleFilter.md +++ b/src/Plugins/SimplnxCore/vv/provenance/IdentifySampleFilter.md @@ -1,122 +1,96 @@ -# Test Data Provenance: IdentifySampleFilter +# Exemplar Archive Provenance: identify_sample_v2.tar.gz -This sidecar records how the test data used by `IdentifySampleFilter`'s unit tests was generated and what oracle class each data set represents. +This sidecar records the archive generation method, independent inline fixtures, and source evidence for IdentifySampleFilter. ---- - -## Archive: `identify_sample_v2.tar.gz` (active) +## Archive identity | Field | Value | |---|---| -| **Data location** | `${DREAM3D_DATA_DIR}/identify_sample_v2/` — 12 `.dream3d` files named `{whole,sliced}_{xy,xz,yz}_{nofill,fill}.dream3d` | -| **Data format** | `.dream3d` (HDF5); each file contains an untouched input `Mask` array and a filter-mutated `Mask Exemplar` array | -| **Used by tests** | `SimplnxCore::IdentifySampleFilter` — 12 `SECTION` variants in `test/IdentifySampleTest.cpp` | -| **Generated by** | `Generate_Exemplars.py`, using the `simplnx` Python bindings directly (confirmed by source inspection — see below) | -| **Generated on** | Input data hand-validated 2026-02-17 by "MAJ" (per `ReadMe.md`); archive file timestamps 2026-03-06 | - -### Generation mechanism (confirmed circular for all 12 files) - -`Generate_Exemplars.py` (in the data directory) does the following for each of the 12 parameter combinations: -1. Creates a `16×14×12` `ImageGeom`. -2. Reads a pre-built mask text file (`generated_data/{whole|sliced}_{XY|XZ|YZ}_{nofill|fill}.txt`) into **both** `Mask` (left untouched — becomes the test's input) and `Mask Exemplar` (becomes the working copy). -3. Runs `nx.IdentifySampleFilter()` **in place on `Mask Exemplar`** — i.e., the "exemplar" is simply the output of SIMPLNX's own filter. -4. Writes both arrays to one `.dream3d` via `WriteDREAM3DFilter`. - -This means **all 12 files are mechanism-circular** — exemplars are produced by SIMPLNX itself, not an independent run of DREAM3D 6.5.171. - -### Input mask design is Class-1-style (hand-placed, documented features) - -`generated_data/ReadMe.md` documents the `16×14×12` input mask as a set of hand-placed rectangular features with known coordinates and known purpose, not random/procedural data: - -| Feature | Extent (X,Y,Z) | Size | Purpose | -|---|---|---|---| -| Main sample | [3,12]×[2,11]×[2,9] | 800 | Intended largest component — the "sample" | -| 3D Hole | [6,9]×[5,8]×[4,7] | 64 | Fully enclosed hole, exercises fill-holes BFS | -| Internal cluster | [7,8]×[6,7]×[5,6] | 8 | Island of good voxels *inside* the hole — purpose-built regression guard, see below | -| Noise A/B/C | 8 each | 8 | Disconnected clusters smaller than the sample in every relevant slice/3D — verify non-sample clusters are cleared | +| **Archive** | `identify_sample_v2.tar.gz` | +| **SHA512** | `a7ffac3eaad479c07215c1dd16274c45a52466708a9d27b5f85a29b0eba3b6705b627e1052a7a27e9bfe89cd6e7df673beb7a1e98b262b6c52ea383b4848ac31` | +| **Used by tests** | `SimplnxCore::IdentifySampleFilter` — 12 sections in `src/Plugins/SimplnxCore/test/IdentifySampleTest.cpp` | +| **Generated by** | Michael Jackson (inferred from PR #1562) | +| **Generated on** | 2026-03-06 to 2026-03-10 (inferred from PR #1562) | +| **Generated at commit** | `42bc9ba6e` (PR #1562 merge, inferred) | -**Internal cluster is a targeted regression test**, not incidental data: per the ReadMe, it catches a known historical bug where the `checked` BFS-visited tracker was not reset between the largest-component pass and the hole-fill pass, which would leave these 8 voxels permanently un-filled. Current source (`IdentifySample.cpp:112`, `checked.assign(totalPoints, false);`) confirms the reset is present, so this fixture currently passes as a guard against reintroducing that regression. +The archive bytes match the SHA512 in `src/Plugins/SimplnxCore/test/CMakeLists.txt`. PR #1562 first registers this archive name. The archived top-level `ReadMe.md` records manual validation by “MAJ” on 2026-02-17; it does not give the archive generation date. -**"8 variations" reconciliation:** the top-level `ReadMe.md` states the data was hand-validated "for all 8 variations," while 12 files exist. This is consistent: per `generated_data/ReadMe.md`, the `plane` parameter is ignored when `slice_by_slice=OFF`, so the 6 `whole_*` files reduce to 2 functionally distinct scenarios (nofill/fill); combined with 6 functionally distinct `sliced_*` scenarios (3 planes × 2 fill modes), that totals 8 unique behaviors — matching the validation claim exactly. +The older `identify_sample.tar.gz` remains registered in CMake but has no consumer in the current IdentifySample test. This sidecar describes the active v2 archive only. -### Stale description in top-level `ReadMe.md` +## How it was generated -The top-level `ReadMe.md` opens by describing a `create_voxel_volume.py` script that generates "a 25×25×25 voxel volume with 3 cylinders cut out... centered along each axis" with random noise. **This does not match the actual archive**: the real geometry is `16×14×12` with hand-placed rectangular features (table above), and no `create_voxel_volume.py` file exists anywhere in the data directory. This paragraph appears to describe an earlier or abandoned generation approach and should not be relied on — the accurate description is `Generate_Exemplars.py` + `generated_data/ReadMe.md`. +The archived `identify_sample_v2/Generate_Exemplars.py` creates a 16×14×12 image from hand-designed text masks in `identify_sample_v2/generated_data/`. It reads each input into both `DataContainer/CellData/Mask` and `DataContainer/CellData/Mask Exemplar`. It runs `IdentifySampleFilter` on `Mask Exemplar`, then writes both arrays with `WriteDREAM3DFilter`. The script generates twelve files named `{whole,sliced}_{xy,xz,yz}_{nofill,fill}.dream3d`. -### Oracle classification +The source mask layout is documented in `identify_sample_v2/generated_data/ReadMe.md`. Coordinates are inclusive. The main-sample extent has 800 cells before the hole is removed. -**Validated-circular** for all 12 files — stronger than a bare circular oracle (the input design is fully documented and analytically understood, and a human reviewer verified the filter's output against that documented design for all 8 functionally distinct scenarios) but **not equivalent to a Class 2 (DREAM3D 6.5.171 reference) oracle**. No claim of legacy numeric parity can be drawn from these 12 files — they verify non-regression of SIMPLNX's own BFS logic (including the specific `checked`-reset regression), not correctness against an independent implementation. +| Feature | X range | Y range | Z range | Cells | Purpose | +|---|---|---|---|---|---| +| Main sample extent | 3–12 | 2–11 | 2–9 | 800 | Largest component. | +| Hole | 6–9 | 5–8 | 4–7 | 64 | Enclosed false region. | +| Internal island | 7–8 | 6–7 | 5–6 | 8 | True island within the hole; checks the visited-flag reset. | +| Noise A | 0–1 | 6–7 | 5–6 | 8 | Disconnected component in XY and XZ slices. | +| Noise B | 7–8 | 0–1 | 0–1 | 8 | Disconnected component in YZ slices. | +| Noise C | 14–15 | 9–10 | 8–9 | 8 | Disconnected component in XY and XZ slices. | -**Coverage gap relative to `IdentifySample-D1`:** this archive's geometry is `16×14×12` — no dimension equals 1 — so it never exercises the dimensionality-dispatch path that `IdentifySample-D1` flags as a deviation. That path is covered separately by the 3 inline non-square 2D Class 1 fixtures in `IdentifySampleTest.cpp` (see below), which remain the only oracle for degenerate-geometry behavior. - -### Recommendation - -The validated-circular classification is acceptable for its actual purpose (regression protection on the 3D BFS algorithm, with a deliberate guard against the `checked`-reset bug). It should **not** be cited as evidence of parity with DREAM3D 6.5.171. If genuine Class 2 confidence is later desired, the same hand-placed feature design documented in `generated_data/ReadMe.md` could be re-run through DREAM3D 6.5.171 directly — no new fixture design would be needed, only re-generation through the legacy tool. Lower-priority alternative: since the feature table already specifies expected behavior per-feature, the per-voxel expected output could be hand-derived from that table and encoded as inline `REQUIRE` assertions, which would upgrade this from "validated circular" to genuine Class 1 without any archive dependency. - ---- - -## Archive: `identify_sample.tar.gz` (v1, orphaned) - -| Field | Value | -|---|---| -| **Data location** | `${DREAM3D_DATA_DIR}/identify_sample/` | -| **Status** | **Orphaned.** Listed in `src/Plugins/SimplnxCore/test/CMakeLists.txt` with a `download_test_data()` entry but no active test references it. Superseded by v2. | -| **Action** | Remove the `download_test_data()` entry from `CMakeLists.txt`. | +The twelve parameter combinations represent eight distinct behaviors: two whole-image fill settings and six slice settings. The plane parameter does not change whole-image execution. The top-level ReadMe's description of a 25×25×25 cylinder volume conflicts with the generation script and the feature-layout ReadMe; it does not describe these fixtures. ---- +## Canonical oracle output -## Inline data: non-square 2D regression tests (Class 1) +The archive provides regression expectations generated by the filter under test. Its outputs are circular references, not an independent correctness oracle. The independent Class 1 expectations are encoded in C++. -| Field | Value | +| DataPath | Source of expected values | |---|---| -| **Data location** | Inline in `src/Plugins/SimplnxCore/test/IdentifySampleTest.cpp` — `CreateNonSquare2DMaskDataStructure()` + `RunIdentifySampleAndCheck()` | -| **Data format** | C++ source (no external file, no SHA512) | -| **Used by tests** | `2D Empty Z Non-Square {3,4,1}`, `2D Empty Y Non-Square {3,1,4}`, `2D Empty X Non-Square {1,3,4}` | -| **Oracle class** | **Class 1 (Analytical)** | -| **Generated by** | Inline test code; expected output is a hard-coded 12-element `bool` array | +| `DataContainer/CellData/Mask Exemplar` | Archived SIMPLNX output, compared with the filtered `DataContainer/CellData/Mask`. | +| `Image/CellData/Mask` in the three non-square cases | Class 1: only indices 0, 1, 3, and 4 remain true. | +| `Image/CellData/Mask` in the hidden HDF5 case | Class 1: geometric membership predicate for the connected region, with the center hole controlled by `FillHoles`. | -### How it was generated - -The fixture builds a `3 × 4` mask (in the two non-empty axes) with a specific pattern: +The non-square input is: ```text -row 0: T T F -row 1: T T F -row 2: F F T -row 3: F F T +T T F +T T F +F F T +F F T ``` -The top-left block (indices 0,1,3,4 in row-major order) is a 4-voxel connected component. The bottom-right block (indices 10,11) is a 2-voxel connected component. After `IdentifySampleFilter` runs (FillHoles=false, SliceBySlice=false), the expected output is the top-left component only: +The four-cell component uses indices 0, 1, 3, and 4. The two-cell component uses indices 8 and 11. The expected mask is: ```text -row 0: T T F -row 1: T T F -row 2: F F F -row 3: F F F +T T F +T T F +F F F +F F F ``` -The same 12-voxel layout is embedded in three different `SizeVec3` geometries: +The same linear arrays are embedded in images of size `{3,4,1}`, `{3,1,4}`, and `{1,3,4}`. Each case asserts all twelve values with whole-image processing and hole filling disabled. These fixtures test connectivity and row stride; they do not test the flat-image hole-fill difference. -- `{3, 4, 1}` — exercises `EmptyZImage2D` dispatch (z=1) -- `{3, 1, 4}` — exercises `EmptyYImage2D` dispatch (y=1) -- `{1, 3, 4}` — exercises `EmptyXImage2D` dispatch (x=1) +The hidden `genuine HDF5 equivalence-page and slice oracle` builds a 129×129×3 image. Its first slice has more than 8000 isolated labels. The next slice joins the cells at `x < 127`, while cells at `x == 128` remain disconnected. A bridge above the interior hole keeps the upper voxel connected to the main component. The expected predicate retains `x < 127` with the checkerboard condition on slice zero; the hole at `(10,10,1)` becomes true only with hole filling enabled. The fixture checks all 49,923 output values for each fill setting and checks the CCL dispatch counter. -All three share identical row-major indexing, so the 12-element `values` and `expected` arrays in the test code are identical for all three. Any wrong row-stride in the dimensionality-templated flood-fill (such as using the 3D neighbor offset for a flat 2D geometry) would either merge the two components into one (incorrect outcome: all 12 voxels retained) or access an out-of-bounds index (test crash). Both failure modes produce a detectable test failure. +The five ordinary cases use resident mask stores. The scenario harness selects BFS and CCL; the default `SIMPLNX_TEST_ALGORITHM_PATH` is 0 (both). All pass in CI at `419f9db35` on Linux, macOS, and Windows in in-core builds. The algorithm and test sources match that commit. -These are the only tests that exercise degenerate-dimension behavior and are therefore the sole evidentiary basis for `IdentifySample-D1`'s claim that SIMPLNX's dimensionality dispatch is correct. +Source inspection establishes the following release exposure. Each row was checked with `git show upstream/release/v:`, not inferred from release dates. ---- +| DREAM3D-NX releases | Inspected source | Boundary and slice behavior | +|---|---|---| +| v7.0.0 through v7.0.1 | `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/IdentifySampleFilter.cpp` | Whole-image coordinate boundary test; single false voxel stays false; no slice mode. | +| v7.0.2 through v7.4.0 | Same filter source path | Same whole-image boundary test; slice ties use `>`; slice fill uses the wrong global index and omits the visited reset. | +| v7.4.1 | `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/IdentifySample.cpp` | Same whole-image boundary test; slice ties still use `>`; slice index and visited reset are corrected. | +| v7.4.2 | Same algorithm source path | Dimensional BFS with the single-voxel boundary guard; slice ties use `>=`; slice index and visited reset remain corrected. | -## Canonical oracle output +`IdentifySample-D1` predates the V&V report. PR #1590 (`e76ad508e`) replaces whole-image coordinate boundary checks with dimensional neighbor checks. That change also creates the single-voxel defect, `IdentifySampleFilter-D2`, between releases. PR #1640 (`a332e7369`) corrects D2 and the slice tie rule (`IdentifySampleFilter-D3`). PR #1562 (`42bc9ba6e`) corrects the slice write index and visited reset (`IdentifySampleFilter-D4` and `IdentifySampleFilter-D5`). -| DataPath | Source of expected values | -|---|---| -| `/Image Geometry/CellData/Mask` (after filter, whole/sliced archive tests) | Validated-circular: produced by SIMPLNX's own filter, human-reviewed against the documented feature table in `generated_data/ReadMe.md`. Not equivalent to a legacy reference. | -| `/Image Geometry/CellData/Mask` (after filter, non-square 2D tests) | Class 1: coded directly in `expected[]` array. | +DREAM.3D tag `v6.5.171`, `Source/Plugins/Processing/ProcessingFilters/IdentifySample.cpp`, uses the coordinate boundary test, resets `checked` before hole filling, and selects a later component on a tie with `>=`. It has no slice mode. This source confirms D1, excludes the legacy application from D2–D5, and explains why the older visited-reset defect was specific to SIMPLNX slice processing. + +PR #1568 (`4ec21e93f`) introduces CCL after the latest listed release. Current `IdentifySampleCCL.cpp` uses all three coordinate extrema to mark boundary roots. For a flat image, this condition marks every false component as exterior. Current `IdentifySampleBFS.cpp` ignores the flat axis, so a 5×5×1 true mask with a false center has different hole-fill behavior between the algorithms (`IdentifySampleFilter-D6`). The existing flat-image tests disable hole filling. This finding is from source inspection, not a new execution. + +## Oracle provenance (Classes 2, 3, 5 only) + +Not applicable. The independent expectations are Class 1 and are encoded in `IdentifySampleTest.cpp`. Neither SIMPLNX-generated archive values nor DREAM.3D output define that oracle. ## Second-engineer oracle review -- **Reviewer:** *Pending* - - *Confirm the feature-table-to-output mapping in `generated_data/ReadMe.md` is plausible for at least the `whole_xy_fill` and `sliced_xy_fill` cases (the two that exercise both the hole-fill BFS and the `checked`-reset regression guard).* - - *Review the non-square 2D fixture layout for plausibility — the 12-element pattern is small enough to verify by hand in ~5 minutes.* -- **Date:** *YYYY-MM-DD (pending)* +- **Reviewer:** Pending. + +## Regenerated to fix a circular-oracle situation? + +No. PR #1562 replaces the v1 test reference with v2 while fixing slice-processing defects. The v2 generation script still uses SIMPLNX to calculate the reference masks. The documented feature layout supports review of these regression fixtures; the inline Class 1 masks provide separate independent checks. diff --git a/src/Plugins/SimplnxCore/vv/provenance/ReplaceElementAttributesWithNeighborValues.md b/src/Plugins/SimplnxCore/vv/provenance/ReplaceElementAttributesWithNeighborValues.md index b468ca3a2a..c697e1a251 100644 --- a/src/Plugins/SimplnxCore/vv/provenance/ReplaceElementAttributesWithNeighborValues.md +++ b/src/Plugins/SimplnxCore/vv/provenance/ReplaceElementAttributesWithNeighborValues.md @@ -1,92 +1,88 @@ -# Test Data Provenance: ReplaceElementAttributesWithNeighborValuesFilter +# Exemplar Archive Provenance: 6_6_replace_element_attributes_with_neighbor.tar.gz + +This sidecar records the archive registration, the independent analytical expectations, and the legacy comparison artifacts. ## Archive identity | Field | Value | -|-------|-------| +|---|---| | **Archive** | `6_6_replace_element_attributes_with_neighbor.tar.gz` | | **SHA512** | `319ebdf08b83ce5ec915afda8ee2af1e0952a3ce26b3e65a4171eb3125c7bc6613c3994610bf526f70413c00da9061c3e6c2d867643220d62faa8c3cd79a96cd` | -| **Used by tests** | None — consuming test removed 2026-07-21 | -| **Generated by** | Unknown (pre-V&V; no generation record found) | -| **Generated at commit** | Unknown | - -## Origin +| **Used by tests** | `SimplnxCore::ReplaceElementAttributesWithNeighborValuesFilter` | +| **Generated by** | Michael Jackson (inferred from PR #400) | +| **Generated on** | 2022-12-30 to 2023-01-24 (inferred from PR #400) | +| **Generated at commit** | `e327b4a71` (PR #400 merge, inferred) | -The archive contains a single `6_6_replace_element_attributes_with_neighbor.dream3d` file generated by running a SIMPLNX pipeline against a Small IN100 subvolume and saving the **post-filter output**. +The first registration of the archive is in PR #400. Its name and SHA512 still match `src/Plugins/SimplnxCore/test/CMakeLists.txt`. -## Circular oracle — problem +## How it was generated -The test loaded this file as both the filter input and the reference exemplar (both `baseDataFilePath` and `exemplarFilePath` pointed to the same path). Because the file contained post-filter data with all CI values already above the threshold, re-running the filter was idempotent (count=0, exits immediately) and the comparison passed trivially. The test confirmed only that the filter is stable on its own output — not that it correctly replaces bad voxels. +PR #400 registers the archive and adds the example pipeline now at `src/Plugins/SimplnxCore/pipelines/ReplaceElementAttributesWithNeighbor.d3dpipeline`. That pipeline reads `Data/Small_IN100/Slice_1.ang`, transforms and crops the data, runs neighbor replacement, and writes a `.dream3d` file. The archive consumer loads `6_6_replace_element_attributes_with_neighbor.dream3d` twice, once as filter input and once as the reference. -## Circular oracle — resolution +The independent inputs are hand-constructed in `BuildSyntheticDataStructure()` in `src/Plugins/SimplnxCore/test/ReplaceElementAttributesWithNeighborValuesTest.cpp`. They use a 3×3×3 `ImageGeom`, a float32 `Confidence Index` array, and an int32 `Marker` array initialized to linear indices. The threshold is 0.5. Origin and spacing do not enter this neighbor-index algorithm; the cubic fixture checks opposite corners and the center, but does not distinguish axis lengths. -The consuming test was removed from `test/ReplaceElementAttributesWithNeighborValuesTest.cpp` on 2026-07-21. The synthetic test (`TEST_CASE "Synthetic neighbor replacement"`) provides a proper Class 1 + Class 4 oracle via `BuildSyntheticDataStructure()` that covers all 16 testable code paths. No replacement archive is needed. +## Canonical oracle output -The archive is unreferenced and may be retired from the Data_Archive release. +The archive supplies a stability comparison, not an independent replacement oracle. Classes 1 and 4 are encoded directly in the synthetic test. ---- +| DataPath | Source of expected values | +|---|---| +| `DataContainer/Cell Data/*` | Archive regression: compare the filtered data with the same file loaded as the reference. | +| `Image3D/CellData/Confidence Index` | Class 1 expected replacement values; Class 4 threshold saturation after loop completion. | +| `Image3D/CellData/Marker` | Class 4 check that replacement changes the center marker from 13. | -## Inlined test data — design and derivation +The analytical fixtures use `index = x + 3*y + 9*z`: -All test data is constructed inline in `test/ReplaceElementAttributesWithNeighborValuesTest.cpp` via `BuildSyntheticDataStructure(goodValue, badValue, badIndices)`. No external files are required. +| Index | Coordinates | Purpose | +|---|---|---| +| 0 | (0,0,0) | Missing -X, -Y, and -Z neighbors | +| 13 | (1,1,1) | Six valid face neighbors | +| 26 | (2,2,2) | Missing +X, +Y, and +Z neighbors | -### Geometry +In the LessThan section, these three cells start at 0.1 and all others start at 0.9. Each failing cell has a passing face neighbor. Each becomes 0.9, and the next pass finds no failing cells. The test checks all 27 values against the threshold, all three replacement values, and `Marker[13] != 13`. -3×3×3 `ImageGeom` (27 voxels). Linear index = `x + y*3 + z*9`. Two cell arrays: -- `Confidence Index` (float32) — the comparison array; initialized to `goodValue` everywhere, then `badValue` at each index in `badIndices`. -- `Marker` (int32) — a second array initialized to the linear index of each voxel. Its only role is to verify that the copy pass propagates **all** AttributeMatrix arrays, not just the comparison array. +The GreaterThan section reverses the values: the three selected cells start at 0.9 and all others at 0.1. Each selected cell becomes 0.1. The test checks all 27 values against the threshold and the three exact replacement values. -### SECTION: LessThan / GreaterThan +The single-pass section starts with only cell 13 at 0.1. It has six passing neighbors at 0.9, so its expected output is 0.9. -`badIndices = {0, 13, 26}`. These three indices were chosen to cover all six boundary cases with the minimum voxel count: +The surrounded-center section starts with bad cells `{4,10,12,13,14,16,22}` and disables the loop. All six neighbors of cell 13 fail the threshold during selection. Its source mark stays -1 and its value stays 0.1. Each surrounding cell has a passing exterior neighbor and becomes 0.9. The test checks the center and all six surrounding values. -| Index | Coordinates (x,y,z) | Boundary condition | -|-------|---------------------|--------------------| -| 0 | (0,0,0) | Corner: -X, -Y, -Z all missing → only +X, +Y, +Z neighbors valid | -| 13 | (1,1,1) | Fully interior: all 6 face neighbors valid | -| 26 | (2,2,2) | Corner: +X, +Y, +Z all missing → only -X, -Y, -Z neighbors valid | +Equal passing candidates exercise the strict best-value comparison. The tests do not assert the exact marker chosen under a tie. They do not directly test the float64 precision correction or a nonnumeric sibling. -No two bad voxels are face-adjacent, so each bad voxel has at least one immediate good neighbor and the first loop iteration resolves all three. +The three ordinary test cases run in Linux, macOS, and Windows in-core builds. All pass in CI at `419f9db35`. The hidden `Generate Test Data` case writes inputs and does not execute replacement. -**LessThan derivation** (`compare = val < threshold`, good=0.9, bad=0.1, threshold=0.5, loop=true): -- Each bad voxel (0.1 < 0.5) has ≥1 face neighbor at 0.9. -- `compare1(0.9, 0.5)` = `0.9 ≥ 0.5` = true; `compare2(0.9, best_init=0.1)` = `0.9 > 0.1` = true → `best` updated to 0.9. -- For subsequent good neighbors at 0.9: `compare2(0.9, 0.9)` = false → no further update (correct: equal neighbors are not "better"). -- Copy pass: all three bad voxels become 0.9; `Marker[13]` becomes the Marker value of voxel 13's chosen neighbor (≠ 13). -- Second loop iteration: all 27 values ≥ 0.5 → count=0 → exits. +## Oracle provenance (Classes 2, 3, 5 only) -**GreaterThan derivation** (`compare = val > threshold`, good=0.1, bad=0.9, threshold=0.5, loop=true): symmetric. All three become 0.1 after one iteration. +Not applicable. Classes 1 and 4 live in the test source. The separate comparison and release evidence follows. -**Asserted invariants:** -- LessThan: `∀i: confStore[i] >= 0.5` and `∀idx ∈ {0,13,26}: confStore[idx] == 0.9` and `markerStore[13] != 13`. -- GreaterThan: `∀i: confStore[i] <= 0.5` and `∀idx ∈ {0,13,26}: confStore[idx] == 0.1`. +`vv_work:ReplaceElementAttributesWithNeighborValuesFilter/comparison_report.md` records the comparison date as 2026-07-21. It identifies the LessThan and GreaterThan comparisons as SIMPLNX versus DREAM.3D v6.5.171: 2 of 2 cases match, with 125 of 125 values identical in both ConfidenceIndex and Marker for each case. It also records a LessThan comparison between DREAM.3D v6.5.171 and a local proof version, with both arrays matching. -### SECTION: loop=false +The inputs are 5×5×5 images. `vv_work:ReplaceElementAttributesWithNeighborValuesFilter/gen_input.py` and `vv_work:ReplaceElementAttributesWithNeighborValuesFilter/gen_input_gt.py` construct them. The failing indices are `{0,12,31,50,60,62,100,124}`. The pipelines are `vv_work:ReplaceElementAttributesWithNeighborValuesFilter/simpl_pipeline.json`, `vv_work:ReplaceElementAttributesWithNeighborValuesFilter/simpl_pipeline_gt.json`, `vv_work:ReplaceElementAttributesWithNeighborValuesFilter/nx_pipeline.d3dpipeline`, and `vv_work:ReplaceElementAttributesWithNeighborValuesFilter/nx_pipeline_gt.d3dpipeline`. -`badIndices = {13}` (center voxel only). With `loop=false`, exactly one scan+copy iteration runs. Voxel 13 has 6 good neighbors at 0.9; the first valid neighbor found sets `best=0.9` and `bestNeighbor[13]`. After the copy pass, `confStore[13] == 0.9`. +The comparison predates these changes to the algorithm files on HEAD: -Asserted: `confStore[13] == Approx(0.9)`. +| Commit | Date | Change | +|---|---|---| +| `4fd0abee9` | 2026-07-22 | PR #1680 preserves the candidate type, clears source marks between passes, and selects numeric sibling arrays before copying. | +| `4ec21e93f` | 2026-09-20 | PR #1568 replaces whole-volume selection and transfer with a rolling comparison window and per-slice transfers. | +| `a33c370ca` | 2026-09-25 | PR #1697 changes the messaging API and throttles progress messages. | -### SECTION: compare1=false skip +These changes come from `git log --since=2026-07-21` on the algorithm `.cpp` and `.hpp`. The current report therefore does not present the July result as a comparison of the current algorithm. -`badIndices = {4, 10, 12, 13, 14, 16, 22}` — center voxel 13=(1,1,1) plus its 6 face neighbors. `loop=false`. +Release exposure was checked by reading `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReplaceElementAttributesWithNeighborValues.cpp` on each of the ten `upstream/release/v` references supplied for this review. D1, D2, and D3 below refer to `ReplaceElementAttributesWithNeighborValuesFilter-D1`, `-D2`, and `-D3`. -The cluster indices map to: +| Released versions | D1: candidate precision | D2: sibling cast | D3: loop without progress | +|---|---|---|---| +| DREAM3D-NX v7.0.0 through v7.4.1 | `float32 best`; affected | Unchecked `dynamic_cast`; affected | Repeats while failing-cell count is positive; affected | +| DREAM3D-NX v7.4.2 | `T best`; fixed | Typed child collection; fixed | Same failing-cell loop; affected | -| Index | Coordinates | Role | -|-------|-------------|------| -| 4 | (1,1,0) | -Z neighbor of 13 | -| 10 | (1,0,1) | -Y neighbor of 13 | -| 12 | (0,1,1) | -X neighbor of 13 | -| 13 | (1,1,1) | Center — target voxel | -| 14 | (2,1,1) | +X neighbor of 13 | -| 16 | (1,2,1) | +Y neighbor of 13 | -| 22 | (1,1,2) | +Z neighbor of 13 | +The DREAM.3D `v6.5.171` source is `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/ReplaceElementAttributesWithNeighborValues.cpp`. It uses `float best`, so D1 affects it. Its copy loop uses the legacy `IDataArray::copyTuple()` interface, without the SIMPLNX reference cast, so D2 does not affect it. Its loop also repeats on the failing-cell count, so D3 affects it. -**Derivation for voxel 13:** Every face neighbor is bad (0.1). For each, `compare1(0.1, 0.5)` = `0.1 ≥ 0.5` = false → `CompareValues` skips the neighbor. `bestNeighbor[13]` remains -1 throughout the scan pass. The copy pass skips voxel 13. `confStore[13]` remains 0.1. +## Second-engineer oracle review -**Derivation for ring voxels** (e.g., voxel 4=(1,1,0)): each has 4 good face neighbors outside the cluster (boundary or good-value voxels). At least one passes `compare1` → gets replaced with 0.9. +- **Reviewer:** skipped +- **Skip reason:** No second engineer available at time of V&V. -**Asserted:** `confStore[13] == Approx(0.1)` (unchanged); `∀idx ∈ {4,10,12,14,16,22}: confStore[idx] == Approx(0.9)` (replaced). +## Regenerated to fix a circular-oracle situation? -This section specifically exercises code path 11 in the coverage table: the `compare1=false` branch of `CompareValues` that skips a neighbor failing the threshold. +No replacement archive was registered. PR #1680 removed the archive consumer and added the surrounded-center analytical section. PR #1568 restored the archive consumer; the current test still loads the same file as input and reference. The four synthetic sections provide the independent correctness checks. diff --git a/src/Plugins/SimplnxCore/vv/provenance/RotateSampleRefFrameFilter.md b/src/Plugins/SimplnxCore/vv/provenance/RotateSampleRefFrameFilter.md index 841a78a682..16e5cc01a7 100644 --- a/src/Plugins/SimplnxCore/vv/provenance/RotateSampleRefFrameFilter.md +++ b/src/Plugins/SimplnxCore/vv/provenance/RotateSampleRefFrameFilter.md @@ -1,44 +1,77 @@ -# Exemplar Provenance: RotateSampleRefFrame (no archive — inline Class 1 oracle) +# Exemplar Archive Provenance: RotateSampleRefFrameFilter (inline oracle) -This filter has **no exemplar archive**. The prior golden-file archives were retired this V&V cycle and replaced by an inlined Class 1 (analytical) + Class 4 (invariant) oracle that lives entirely in the test source. This sidecar records that decision and the provenance of the legacy A/B inputs. +This filter uses inline Class 1 and Class 4 expectations. It has no active exemplar archive. ---- - -## Retired archive +## Archive identity | Field | Value | |---|---| -| **Retired archives** | `Rotate_Sample_Ref_Frame_Test_v2.tar.gz`, `Rotate_Sample_Ref_Frame_Test_v3.tar.gz` | -| **Retired on** | 2026-07-02 | -| **Where they were referenced** | `src/Plugins/SimplnxCore/test/CMakeLists.txt` `download_test_data()` (entries removed) and the old `SimplnxCore::RotateSampleRefFrame` exemplar test (rewritten) | -| **Why retired** | Golden-file / regression oracle: the test compared filter output against a bundled `.dream3d` of previously-captured output, which cannot establish correctness independently (circular-oracle pattern per `oracle_classes.md`). The prior test also included 45-degree rotation cases that are now (correctly) rejected by the preflight guard. | +| **Archive** | None. `Rotate_Sample_Ref_Frame_Test_v2.tar.gz` and `Rotate_Sample_Ref_Frame_Test_v3.tar.gz` are retired. | +| **SHA512** | Not applicable. `src/Plugins/SimplnxCore/test/CMakeLists.txt` has no active rotation archive registration. | +| **Used by tests** | Nine ordinary analytical or invariant cases in `src/Plugins/SimplnxCore/test/RotateSampleRefFrameTest.cpp` use inline fixtures. | +| **Generated by** | Not applicable; the tests construct their inputs. | +| **Generated on** | Not applicable; inputs are constructed on each test invocation. | +| **Generated at commit** | Inline analytical tests introduced in `790d9ca18` (PR #1651). | + +## How it was generated + +`CreateSequentialImageGeom()` builds synthetic Image Geometry fixtures with scalar Int32 values 1 through N. Hand-derived arrays specify the expected rotations; the tests also check dimensions, origins, spacing and value conservation. The separate legacy comparison uses `vv_work:RotateSampleRefFrameFilter/make_input.py` to write a shared 4×3×2 input with origin (0,0,0), spacing (1,1,1) and values 1 through 24. `vv_work:RotateSampleRefFrameFilter/gen_simpl_pipeline.py` and `vv_work:RotateSampleRefFrameFilter/gen_nx_pipeline.py` generate the four comparison pipelines for each application. -## Canonical oracle output (inline, no archive) +## Canonical oracle output -| Oracle | Source of expected values | +| DataPath | Source of expected values | |---|---| -| 180@Z on a 3×2×1 volume → `[6,5,4,3,2,1]` | Class 1 hand derivation (a 180-degree rotation about Z reverses a single Z-slice's row-major order) — inline in `test/RotateSampleRefFrameTest.cpp` | -| Output dimensions for all 9 principal-90 rotations | Class 1 (deterministic axis permutation of the input dims) — inline | -| Value-multiset conservation, zero background, full-circle composition = identity | Class 4 invariants — inline predicates | -| `KeepInputGeometryOrigin` origin values `(0,0,0)` / `(-3,0,0)` | Class 1 hand derivation (rotated bounding-box min for 90@Z of a 4×3×2 at origin 0) — inline | +| `Output/CellData/Values` | Class 1: 180 degrees about Z on a 3×2×1 input gives [6,5,4,3,2,1]. | +| `Output/CellData/Values` | Class 1: the three 90-degree chirality fixtures give [4,1,5,2,6,3] for Z and X, and [3,6,2,5,1,4] for Y. | +| `Output` geometry | Class 1: axis permutation gives dimensions; 90 degrees about Z changes spacing (2,5,1) to (5,2,1). | +| `Output` geometry | Class 1: rotating the bounding box at origin (10,20,0) gives origin (-22,10,0); keep-origin preserves (10,20,0). | +| `Step_3/CellData/Values` or `Step_1/CellData/Values` | Class 4: four quarter turns or two half turns restore the original values and dimensions. | +| `SliceBySlice/CellData/Values`, `Full3D/CellData/Values` | Class 4: both modes conserve the input values; a half turn about Y gives different permutations. | + +The nine ordinary positive cases include 18 principal-axis rotation variants and three chirality fixtures. The 13 ordinary tests, including rejection, conversion and child-name cases, use in-core CI builds on Linux, macOS and Windows. All pass in CI at `419f9db35`. The current test source matches that revision. + +The hidden HDF5 fixture constructs 257×257×4 values. Its expected value at flat index `i` is `plane * 66049 + 66049 - inPlane`, where `plane = i / 66049` and `inPlane = i % 66049`. It asserts every value, both slice modes, store types and dispatch counters. These are test-source facts, separate from the ordinary CI result. + +Release-source inspection of `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/RotateSampleRefFrameFilter.cpp` and `src/simplnx/Utilities/ImageRotationUtilities.cpp` gives: -Classes 1 and 4 require no external provenance block — the oracle lives in the test code. +| Released versions | D1 guards | D2 origin addition | D3 path substring replacement | +|---|---|---|---| +| DREAM3D-NX v7.0.0 through v7.4.1 | Absent | Present; bounds already include input origin | Present | +| DREAM3D-NX v7.4.2 | Present | Fixed; assigns the absolute transformed minimum | Present | -## Legacy A/B comparison inputs (provenance) +All ten release snapshots were read with `git show upstream/release/v:`. PR #1651 (`790d9ca18`) adds the guards and fixes D2. PR #1765 (`1986127eb`) fixes D3 with path-prefix rebasing. Tagged DREAM.3D v6.5.171 `Source/Plugins/Sampling/SamplingFilters/RotateSampleRefFrame.cpp` has no corresponding origin-addition or child-copy operation; it was not affected by D2 or D3. -The DREAM3D 6.5.171 comparison used a shared input minted with the `legacy_dream3d` h5py writer (a legacy v7/SIMPL `.dream3d` readable by both PipelineRunner and nxrunner): +## Oracle provenance (Classes 2, 3, 5 only) -- **Generator:** `vv/comparisons/RotateSampleRefFrameFilter/make_input.py` — 4×3×2 Image Geometry, origin (0,0,0), spacing (1,1,1), Int32 `Data = 1..24` (ZYX). -- **Pipelines:** `gen_simpl_pipeline.py` (legacy `.json`) and `gen_nx_pipeline.py` (NX `.d3dpipeline`), 4 cases each (90@Z, 180@Z, 90@X, 180@Y). -- **Result:** bit-identical on all four fixtures — `vv/comparisons/RotateSampleRefFrameFilter/results.md`. +Not applicable. The correctness oracle is Class 1 with Class 4 companions and lives in the test source. The following comparison is separate from that oracle. -These A/B artifacts are working files, not a test-consumed archive; they are kept in the source tree for reproducibility only. +Run against DREAM.3D v6.5.171 on 2026-07-02: 4 of 4 rotation cases match. `vv_work:RotateSampleRefFrameFilter/results.md` identifies the official application `PipelineRunner` and records identical dimensions and every voxel value for 90 degrees about Z, 180 degrees about Z, 90 degrees about X and 180 degrees about Y. Both applications rotate in place. No ReadMe is present; `results.md` supplies the environment and run date. + +This comparison predates the current algorithm. The source history after 2026-07-02 contains the following changes to `Algorithms/RotateSampleRefFrame.cpp` and the shared `ImageRotationUtilities.hpp` and `.cpp` files: + +| Commit | Date | Change | +|---|---|---| +| `790d9ca18` | 2026-07-10 | V&V changes and rotation guards; the filter preflight also fixes the output origin. | +| `4ec21e93f` | 2026-09-20 | Storage architecture and algorithm changes. | +| `a33c370ca` | 2026-09-25 | Filter messaging API changes. | +| `62000012e` | 2026-10-20 | Loop progress uses the progress messaging API. | + +The filter preflight also changed in `9ed922f2b` (2026-09-29, stale AttributeMatrix feedback) and `1986127eb` (2026-10-09, child-path correction). The saved comparison therefore does not demonstrate parity for the current implementation. ## Second-engineer oracle review -- **Reviewer:** Michael Jackson (technical authority) -- **Date:** 2026-07-16 +- **Reviewer:** Michael Jackson (technical authority). +- **Date:** 2026-07-16. + +The report sign-off names Michael Jackson as the first engineer and names no separate second engineer. ## Regenerated to fix a circular-oracle situation? -Yes — this replaces the retired `Rotate_Sample_Ref_Frame_Test_v2/v3.tar.gz` golden-file archives with an independently-derived Class 1 analytical oracle. The new oracle is *not* derived from SIMPLNX (or legacy) output; it is hand-derived from the definition of a principal-90 reference-frame rotation. +The archives were retired, not regenerated. PR #1651 (`790d9ca18`) removes their download registrations and replaces the prior output comparisons with independent inline expectations. The current CMake file documents this retirement. The replacement expectations come from analytical permutations and invariants, not captured filter output. + +The first archive registrations provide the following inferred generation records: + +| Retired archive | Generated by | Generated on | Generated at commit | +|---|---|---|---| +| `Rotate_Sample_Ref_Frame_Test_v2.tar.gz` | Michael Jackson (inferred from PR #1355) | 2025-07-03 to 2025-07-03 (inferred from PR #1355) | `ce68a49ae` (PR #1355 merge, inferred) | +| `Rotate_Sample_Ref_Frame_Test_v3.tar.gz` | Joey Kleingers (inferred from PR #1465) | 2025-11-04 to 2025-11-10 (inferred from PR #1465) | `531893fbd` (PR #1465 merge, inferred) | diff --git a/src/Plugins/SimplnxCore/vv/provenance/dbscan_test.md b/src/Plugins/SimplnxCore/vv/provenance/dbscan_test.md index 48ff8b8ce4..ade8767ee8 100644 --- a/src/Plugins/SimplnxCore/vv/provenance/dbscan_test.md +++ b/src/Plugins/SimplnxCore/vv/provenance/dbscan_test.md @@ -1,9 +1,5 @@ # Exemplar Archive Provenance: dbscan_test.tar.gz -This sidecar records how an exemplar archive used in unit tests was generated. It is the answer to "where did this gold-standard data come from?" - ---- - ## Archive identity | Field | Value | @@ -11,47 +7,41 @@ This sidecar records how an exemplar archive used in unit tests was generated. I | **Archive** | `dbscan_test.tar.gz` | | **SHA512** | `77d7886e2550b63176b564e827d7de320b5a28b1c8a55bf107d53acd6962757275bc86b3382ff789f612a6838f55ab8f4af29435aec83a7a804b9487e57a6386` | | **Used by tests** | `SimplnxCore::DBSCAN: 2D Test: Aniso`, `SimplnxCore::DBSCAN: 2D Test: Blobs`, `SimplnxCore::DBSCAN: 2D Test: Noisy Circles`, `SimplnxCore::DBSCAN: 2D Test: Noisy Moons`, `SimplnxCore::DBSCAN: 2D Test: No Structure`, `SimplnxCore::DBSCAN: 2D Test: Varied`, `SimplnxCore::DBSCAN: 3D Test (LowDensityFirst)` | -| **Generated by** | Nathan Young | -| **Generated on** | *approximately 2025-09-04 (PR #1421 merge date — exact date TBD)* | -| **Generated at commit** | `c1bc9114b7` (PERF/ENH: DBSCAN Rewrite) | +| **Generated by** | Nathan Young (inferred from PR #1421) | +| **Generated on** | No generation date is recorded. | +| **Generated at commit** | `c1bc9114b7` (archive registration in PR #1421, 2025-09-04) | ## How it was generated -The archive contains two input+output `.dream3d` files: +The archive contains `7_0_2d_dbscan_test_data.dream3d` and `7_0_3d_dbscan_test_data.dream3d`, with XDMF companions. The script `vv_work:DBSCANFilter/dbscan_data_proj/plot_cluster_comparison.py` generates six synthetic 500-by-2 input datasets with scikit-learn and scales them with `StandardScaler`. SIMPLNX GDCF output from `LowDensityFirst` supplies the saved cluster labels; the 3D case uses epsilon 0.01 and minimum points 5. The 3D input source and generation script are not recorded. -1. **`7_0_2d_dbscan_test_data.dream3d`** — six 2D floating-point datasets derived from scikit-learn's toy clustering benchmarks. The input point arrays were generated by `dbscan_vv/dbscan_data_proj/plot_cluster_comparison.py` using deterministic seeds (seed=30 for circles/moons/blobs/no_structure; random_state=170 for aniso/varied) and scaled with `sklearn.preprocessing.StandardScaler`. The script saves 500×2 float64 CSVs for each dataset. Each dataset was then run through `DBSCANFilter` (post-PR #1421 GDCF rewrite) using `LowDensityFirst` parse order with the hyperparameters listed in `docs/DBSCANFilter.md`. The resulting cluster-ID arrays were saved as the exemplar arrays. +PR #1421 added the archive registration on 2025-09-04. Its author identifies the inferred generator; the merge date does not give the archive generation date. The available local evidence does not give the date when that PR opened. -2. **`7_0_3d_dbscan_test_data.dream3d`** — a 3D reduced vertex geometry dataset (origin and generation script unknown) run through `DBSCANFilter` (ε=0.01/minPts=5/LDF). Exemplar cluster IDs saved as `VertexData/Cluster Ids`. - -The `LowDensityFirst` cluster-label arrays in both files were generated by running SIMPLNX itself after the PR #1421 rewrite and saving the output — at the time of creation, the label arrays were a circular oracle. The *input point arrays* are independently sourced from scikit-learn and reproducible. The circularity was resolved in Phase 6 by running scikit-learn's own DBSCAN on the same input data as an independent oracle; see "Circular-oracle resolution" below. The 3D exemplar has no external oracle and remains circular. +## Canonical oracle output -### Input data generation environment +The following DataPaths are the saved regression labels. They are SIMPLNX outputs, not independently generated expected labels. The Class 2 oracle supplies cluster counts, noise counts and sorted cluster sizes for the 2D inputs. -| Tool | Version | +| DataPath | Source of expected values | |---|---| -| scikit-learn | 1.7.1 | -| numpy | 2.2.6 | -| scipy | 1.15.3 | -| Python | 3.10 | -| venv location | `dbscan_vv/dbscan_data_proj/venv/` | -| Generation script | `dbscan_vv/dbscan_data_proj/plot_cluster_comparison.py` | +| `/AnisoGeometry/Vertex Data/Aniso Cluster Ids` | SIMPLNX regression labels; aggregate results differ from scikit-learn as described by `DBSCANFilter-D1`. | +| `/BlobsGeometry/Vertex Data/Blobs Cluster Ids` | SIMPLNX regression labels; cluster count, noise count and sorted sizes match scikit-learn. | +| `/CirclesGeometry/Vertex Data/Circles Cluster Ids` | SIMPLNX regression labels; cluster count, noise count and sorted sizes match scikit-learn. | +| `/MoonsGeometry/Vertex Data/Moons Cluster Ids` | SIMPLNX regression labels; cluster count, noise count and sorted sizes match scikit-learn. | +| `/NoStructureGeometry/Vertex Data/No Structure Cluster Ids` | SIMPLNX regression labels; cluster count, noise count and sorted sizes match scikit-learn. | +| `/VariedGeometry/Vertex Data/Varied Cluster Ids` | SIMPLNX regression labels; aggregate results differ from scikit-learn as described by `DBSCANFilter-D1`. | +| `/Reduced Vertex Geom/VertexData/Cluster Ids` | SIMPLNX regression labels; no independent oracle is recorded for this 3D input. | -## Canonical oracle output +## Oracle provenance (Classes 2, 3, 5 only) -| DataPath | Source of expected values | Status | -|---|---|---| -| `aniso/AnisoGeometry/VertexData/Aniso Cluster Ids` | `make_blobs(random_state=170)` + linear transform `[[0.6,-0.6],[-0.4,0.8]]`, verified vs sklearn 1.7.1 oracle (Phase 6) | ✅ Regression fixture — DBSCAN-D1 deviation documented | -| `blobs/BlobsGeometry/VertexData/Blobs Cluster Ids` | sklearn `make_blobs`, verified vs sklearn 1.7.1 oracle (Phase 6) | ✅ Regression fixture — exact sklearn match | -| `noisy_circles/CirclesGeometry/VertexData/Circles Cluster Ids` | sklearn `make_circles`, verified vs sklearn 1.7.1 oracle (Phase 6) | ✅ Regression fixture — exact sklearn match | -| `noisy_moons/MoonsGeometry/VertexData/Moons Cluster Ids` | sklearn `make_moons`, verified vs sklearn 1.7.1 oracle (Phase 6) | ✅ Regression fixture — exact sklearn match | -| `no_structure/NoStructureGeometry/VertexData/No Structure Cluster Ids` | sklearn uniform random, verified vs sklearn 1.7.1 oracle (Phase 6) | ✅ Regression fixture — exact sklearn match | -| `varied/VariedGeometry/VertexData/Varied Cluster Ids` | sklearn `make_blobs` (varied std), verified vs sklearn 1.7.1 oracle (Phase 6) | ✅ Regression fixture — DBSCAN-D1 deviation documented | -| `Reduced Vertex Geom/VertexData/Cluster Ids` | 3D dataset of unknown origin — no external oracle applied | ⚠️ Circular oracle — unresolved | +### Class 2 — Reference implementation -## Class 2 oracle (scikit-learn 1.7.1) — run 2026-08-04 +- **Library:** scikit-learn `sklearn.cluster.DBSCAN`. +- **Exact version:** 1.7.1, recorded in `vv_work:DBSCANFilter/oracle_results.json`. +- **Runtime:** Python 3.10, NumPy 2.2.6 and SciPy 1.15.3, recorded in `vv_work:DBSCANFilter/run_sklearn_oracle.py`. +- **Random seed:** Input generation uses 30 for circles, moons, blobs and no_structure, and 170 for aniso and varied. The reference DBSCAN call has no random seed. +- **Script in archive:** None. The reference script is `vv_work:DBSCANFilter/run_sklearn_oracle.py`. -Script: `dbscan_vv/run_sklearn_oracle.py` -Results: `dbscan_vv/oracle_results.json` +The reference script reads the six input files and writes the following values to `vv_work:DBSCANFilter/oracle_results.json`. The saved result key `ansio` refers to the Aniso dataset. | Dataset | ε | minPts | sklearn n_clusters | sklearn n_noise | sklearn cluster_sizes (sorted) | |---|---|---|---|---|---| @@ -62,56 +52,28 @@ Results: `dbscan_vv/oracle_results.json` | no_structure | 0.30 | 3 | 1 | 0 | [500] | | varied | 0.18 | 3 | 11 | 47 | [3, 3, 3, 4, 4, 4, 5, 5, 87, 166, 169] | -These are the Class 2 oracle expected values. SIMPLNX cluster counts and sizes should match (with possible small variation in `ansio` and `varied` due to Deviation DBSCAN-D1 at cluster boundaries). - ---- +`vv_work:DBSCANFilter/compare_exemplar_vs_oracle.py` reads the saved SIMPLNX labels and compares their aggregates with the reference results. Its output is `vv_work:DBSCANFilter/phase6_comparison_results.json`. Four datasets have equal cluster counts and sorted sizes. The `ansio` and `varied` datasets have 3 SIMPLNX clusters, compared with 6 and 11 reference clusters. These differences are recorded as `DBSCANFilter-D1`. -## Oracle provenance — circular oracle finding and resolution (cluster-label arrays) +`vv_work:DBSCANFilter/ab_6_5_171.py` runs the legacy filter and compares its output with the saved reference and SIMPLNX aggregates. `vv_work:DBSCANFilter/ab_6_5_171_comparison_results.json` contains the following values. Each result cell gives the cluster count, noise count and sorted cluster sizes. -> **Was circular at creation.** The cluster-label arrays in this archive were produced by running `DBSCANFilter` (post-PR #1421 rewrite, LDF mode) on the sklearn-sourced input data and saving the output. The inline test comment reads: *"The exemplars were generated with LDF."* PR #1421 stated the cases were "validated externally," but the mechanism was not documented. Per V&V policy, this is circular: any bug present at capture time becomes the new "correct" answer. -> -> **Resolved in Phase 6 (2026-08-04).** The independent scikit-learn 1.7.1 oracle (same library used to generate the input data) was run against the same input files with the same parameters. Results confirmed 4/6 exact match (blobs, noisy_circles, noisy_moons, no_structure); 2 deviations (ansio, varied) explained by DBSCAN-D1. The 6 affected 2D cluster-label arrays are now regression fixtures. The 3D exemplar remains unresolved (unknown input origin, no external oracle). +| Dataset | ε | minPts | DREAM.3D v6.5.171 | scikit-learn 1.7.1 | Saved SIMPLNX labels | +|---|---|---|---|---|---| +| ansio | 0.15 | 4 | 6 clusters, 24 noise, [1, 5, 6, 152, 155, 157] | 6 clusters, 21 noise, [4, 5, 6, 152, 155, 157] | 3 clusters, 30 noise, [153, 156, 161] | +| blobs | 0.30 | 3 | 2 clusters, 7 noise, [164, 329] | 2 clusters, 7 noise, [164, 329] | 2 clusters, 7 noise, [164, 329] | +| noisy_circles | 0.30 | 3 | 2 clusters, 0 noise, [250, 250] | 2 clusters, 0 noise, [250, 250] | 2 clusters, 0 noise, [250, 250] | +| noisy_moons | 0.30 | 3 | 2 clusters, 0 noise, [250, 250] | 2 clusters, 0 noise, [250, 250] | 2 clusters, 0 noise, [250, 250] | +| no_structure | 0.30 | 3 | 1 cluster, 0 noise, [500] | 1 cluster, 0 noise, [500] | 1 cluster, 0 noise, [500] | +| varied | 0.18 | 3 | 11 clusters, 48 noise, [3, 3, 3, 3, 4, 4, 5, 5, 87, 166, 169] | 11 clusters, 47 noise, [3, 3, 3, 4, 4, 4, 5, 5, 87, 166, 169] | 3 clusters, 78 noise, [87, 166, 169] | -### What was done to resolve it +The four matching rows show equality of these aggregates, not equality of every point label. For `ansio` and `varied`, DREAM.3D v6.5.171 and scikit-learn have equal cluster counts but different noise counts and sorted sizes. The script reads saved SIMPLNX aggregates and does not execute the current algorithm. A comparison with current execution output is required before the report can claim a current legacy comparison. -1. ✅ **Independent oracle established** — scikit-learn 1.7.1 DBSCAN run on the same `.txt` input files via `dbscan_vv/run_sklearn_oracle.py`. Results in `dbscan_vv/oracle_results.json`. -2. ✅ **SIMPLNX verified against oracle** (Phase 6) — 4/6 exact match; 2 deviations explained by DBSCAN-D1 and corroborated by Phase 9 legacy comparison. -3. ✅ **LDF exemplar arrays promoted to regression fixtures** — no regeneration needed. -4. **3D exemplar not resolved** — no external oracle available for the 3D dataset. Remains circular; correctness claim rests on SIMPLNX's own output only. +The report's 15 registered tests and one hidden OOC test correspond to `DBSCANTest.cpp` at `65a3038e`, which includes the mask correction associated with PR #1766. This documentation checkout has 12 registered tests and the hidden OOC test; it lacks the three mask regression tests and the mask correction. The report carries the recorded CI result for the corrected source. The OOC suite has no recorded execution result. None of these test cases has a `[!shouldfail]` tag. ## Second-engineer oracle review -- **Reviewer:** *pending* -- **Date:** *pending* -- **Skip reason** (if skipped): *To be filled in — record reason if second engineer is not available.* - -## Circular-oracle resolution (Phase 6, 2026-08-04) - -The circular-oracle situation identified in Phase 2 was resolved in Phase 6 reconciliation (2026-08-04): - -- Script: `dbscan_vv/compare_exemplar_vs_oracle.py` -- Results: `dbscan_vv/phase6_comparison_results.json` - -**Finding**: The Class 2 sklearn oracle independently confirmed that SIMPLNX is correct for 4/6 datasets (blobs, noisy_circles, noisy_moons, no_structure — exact cluster count and sizes). The 2 deviating datasets (ansio, varied) diverge from sklearn only in micro-clusters at or below the minPts threshold — consistent with Deviation DBSCAN-D1 (GDCF grid-cell vs. point-level core definition). These are not bugs. - -**Status change**: The LDF exemplar arrays in this archive are **promoted from "circular oracle" to "regression fixtures"** as of 2026-08-04. They remain in place — no regeneration required. The primary correctness proof is now the Class 2 sklearn oracle comparison recorded in the V&V working document (`src/Plugins/SimplnxCore/vv/DBSCANFilter.md`, Phase 6). - -This archive has **not** been regenerated. Regeneration is not needed because the independent oracle confirmed the existing arrays are correct. - -## Legacy comparison corroboration (Phase 9, 2026-08-05) - -Script: `dbscan_vv/phase9_ab_test.py` -Results: `dbscan_vv/phase9_comparison_results.json` - -DREAM3D 6.5.172 (traditional DBSCAN, via `DREAM3DReview` plugin UUID `c2d4f1e8`) was run against the same 6 sklearn datasets. Three-way comparison (legacy 6.5.172 vs. sklearn 1.7.1 vs. SIMPLNX): +- **Reviewer:** Michael Jackson +- **Date:** 2026-08-28 -| Dataset | Legacy n_clusters | sklearn n_clusters | SIMPLNX n_clusters | Match | -|---|---|---|---|---| -| ansio | 6 | 6 | 3 | Legacy = sklearn; SIMPLNX differs (DBSCAN-D1) | -| blobs | 2 | 2 | 2 | Exact three-way match | -| noisy_circles | 2 | 2 | 2 | Exact three-way match | -| noisy_moons | 2 | 2 | 2 | Exact three-way match | -| no_structure | 1 | 1 | 1 | Exact three-way match | -| varied | 11 | 11 | 3 | Legacy = sklearn; SIMPLNX differs (DBSCAN-D1) | +## Regenerated to fix a circular-oracle situation? -**Conclusion**: The regression fixtures in this archive are consistent with both the Class 2 sklearn oracle (confirmed Phase 6) and the legacy comparison (confirmed Phase 9). The two deviating datasets (ansio, varied) are correctly attributed to DBSCAN-D1 (GDCF grid-cell core definition vs. traditional point-level core definition). The archive status remains **regression fixtures** — no regeneration required. +No. The archive retains the SIMPLNX-generated labels. The scikit-learn comparison checks aggregates for the six 2D inputs: four match and two have the documented GDCF difference. These comparisons support use of the 2D labels as regression fixtures; they do not independently verify every saved label. The 3D labels remain a circular oracle because no independent 3D comparison is recorded.