From 65aab25e7b29a3063a4f4c42679519065d052990 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Fri, 2 Oct 2026 15:15:55 -0400 Subject: [PATCH 01/91] DOC: Add a defect hunt to the filter V&V policy An oracle shows that a filter agrees with the fixtures that the engineer chose. It does not show that those fixtures can find a defect. Issue #1758 (AlignGeometries Centroid mode uses the X offset on all axes) shows the gap: no test ran that mode, and a fixture with equal per-axis offsets would also pass. * Add defect_hunt.md: bug-pattern review, mode coverage, symmetry-breaking fixtures, mutation check, and metamorphic relations * vv_policy.md: require the defect hunt after code-path enumeration * report_gates.md: add the Defect hunt section and row, the symmetry gate for oracle fixtures, and the per-mode value-assertion gate * report_template.md: add the Defect hunt section and row * oracle_classes.md: add metamorphic relations to Class 4 * Reports that reached READY FOR REVIEW before 2026-10-02 are not required to add the section until their next V&V change Co-Authored-By: Claude Opus 5.5 --- docs/vv_templates/defect_hunt.md | 81 ++++++++++++++++++++++++++++ docs/vv_templates/oracle_classes.md | 1 + docs/vv_templates/report_gates.md | 16 +++++- docs/vv_templates/report_template.md | 15 ++++++ docs/vv_templates/vv_policy.md | 16 ++++-- 5 files changed, 124 insertions(+), 5 deletions(-) create mode 100644 docs/vv_templates/defect_hunt.md diff --git a/docs/vv_templates/defect_hunt.md b/docs/vv_templates/defect_hunt.md new file mode 100644 index 0000000000..becdc2947b --- /dev/null +++ b/docs/vv_templates/defect_hunt.md @@ -0,0 +1,81 @@ +# 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. | + +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/oracle_classes.md b/docs/vv_templates/oracle_classes.md index 1fadb93438..4c76aa3b22 100644 --- a/docs/vv_templates/oracle_classes.md +++ b/docs/vv_templates/oracle_classes.md @@ -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:** diff --git a/docs/vv_templates/report_gates.md b/docs/vv_templates/report_gates.md index 6ffbdadbd8..ae86394237 100644 --- a/docs/vv_templates/report_gates.md +++ b/docs/vv_templates/report_gates.md @@ -17,7 +17,7 @@ A section of `src/Plugins/

/vv/.md` is "done" when all its gates p 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" @@ -25,6 +25,7 @@ The dashboard a reviewer reads first. Lets a reviewer decide in 30 seconds wheth - [ ] **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") +- [ ] **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 +58,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 +72,18 @@ 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` ## Test inventory diff --git a/docs/vv_templates/report_template.md b/docs/vv_templates/report_template.md index b8f7867bf4..25e811228f 100644 --- a/docs/vv_templates/report_template.md +++ b/docs/vv_templates/report_template.md @@ -23,6 +23,7 @@ A scannable dashboard for reviewers. Each row is one sentence to one short parag | 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.* | +| 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.* | @@ -59,6 +60,20 @@ For worked instances see `src/Plugins/OrientationAnalysis/vv/BadDataNeighborOrie | `-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.* | +## 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.* + +*Mutation check:* *K of N mutants killed. List each survivor and its disposition.* + +*Metamorphic relations:* *e.g. translation by (1, 2, 3) moves the output by (1, 2, 3)* / *None.* + ## Code path coverage *N of M paths exercised. If N < M: which paths are NOT covered, and why each gap is acceptable (or what would close it).* diff --git a/docs/vv_templates/vv_policy.md b/docs/vv_templates/vv_policy.md index 2a58017b98..21ed467169 100644 --- a/docs/vv_templates/vv_policy.md +++ b/docs/vv_templates/vv_policy.md @@ -18,6 +18,12 @@ Each filter must first be shown correct **independently** of 6.5.171 against an 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,11 @@ 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 | ## Engineer workflow @@ -64,9 +71,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 From 34d9a1feec438024aaa007aeb3b2d385e790d1c1 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 7 Oct 2026 18:51:26 -0400 Subject: [PATCH 02/91] DOC: Add SIMPLNX input-shape patterns to the defect hunt checklist * Add three Part 1 bug patterns: component shape not declared on the selection parameter, parent Attribute Matrix not checked, and feature-level bounds * State where each kind of check belongs: structure in the parameter, relationships between inputs in preflight, value-dependent bounds in execute * Add a "SIMPLNX vocabulary for findings" section with the Attribute Matrix kinds and an example of SIMPLNX wording Co-Authored-By: Claude Opus 5.5 --- docs/vv_templates/defect_hunt.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/vv_templates/defect_hunt.md b/docs/vv_templates/defect_hunt.md index becdc2947b..1298142411 100644 --- a/docs/vv_templates/defect_hunt.md +++ b/docs/vv_templates/defect_hunt.md @@ -24,6 +24,22 @@ Read every line of the algorithm, its helpers, and the filter's `preflightImpl() | **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. From d3082d35f468ea44cf211a83b64b9dc7970dbb61 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 7 Oct 2026 19:00:55 -0400 Subject: [PATCH 03/91] DOC: Reference DREAM3D 6.5.171 only in V&V documents V&V documents and test comments named DREAM3D 6.5.172, which is a private proof branch and was never released. Each reference now names DREAM3D 6.5.171 or describes the locally patched legacy build; no sentence claims that 6.5.171 contains a fix that only the patched build had. The FillBadData summary no longer cites the withdrawn three-way A/B run as evidence. The DBSCAN files are left for #1766. Co-Authored-By: Claude Opus 5.5 --- .../docs/ComputeFaceIPFColoringFilter.md | 2 +- .../OrientationAnalysis/test/ComputeAvgCAxesTest.cpp | 2 +- .../vv/deviations/ComputeFaceIPFColoringFilter.md | 10 +++++----- .../vv/deviations/WritePoleFigureFilter.md | 2 +- .../vv/provenance/WritePoleFigureFilter.md | 4 ++-- .../SimplnxCore/test/ComputeGroupingDensityTest.cpp | 2 +- .../SimplnxCore/vv/ComputeFeatureCentroidsFilter.md | 2 +- .../SimplnxCore/vv/ComputeFeatureSizesFilter.md | 2 +- src/Plugins/SimplnxCore/vv/FillBadDataFilter.md | 2 +- ...ReplaceElementAttributesWithNeighborValuesFilter.md | 6 +++--- .../vv/deviations/ComputeFeatureCentroidsFilter.md | 2 +- .../vv/deviations/ComputeFeatureSizesFilter.md | 2 +- .../SimplnxCore/vv/deviations/FillBadDataFilter.md | 2 +- ...ReplaceElementAttributesWithNeighborValuesFilter.md | 2 +- src/Plugins/SimplnxCore/vv/provenance/dbscan_test.md | 2 +- 15 files changed, 22 insertions(+), 22 deletions(-) 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/test/ComputeAvgCAxesTest.cpp b/src/Plugins/OrientationAnalysis/test/ComputeAvgCAxesTest.cpp index aed6e16e3f..f161da0a7a 100644 --- a/src/Plugins/OrientationAnalysis/test/ComputeAvgCAxesTest.cpp +++ b/src/Plugins/OrientationAnalysis/test/ComputeAvgCAxesTest.cpp @@ -72,7 +72,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; diff --git a/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFaceIPFColoringFilter.md b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFaceIPFColoringFilter.md index acecda1a44..0021feced8 100644 --- a/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFaceIPFColoringFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFaceIPFColoringFilter.md @@ -21,13 +21,13 @@ Entries are referenced by stable ID (`ComputeFaceIPFColoringFilter-D`) from t - *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. -The fix (`phase1`→`phase2`) is confirmed against the Class 1 analytical oracle and matches the legacy 6.5.172 backport `1c96b3b8e`. +The fix (`phase1`→`phase2`) is confirmed against the Class 1 analytical oracle and matches the backport in a locally patched legacy build `1c96b3b8e`. **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 +face's Phase-2 color is black `(0,0,0)`. A locally patched legacy build 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. @@ -42,7 +42,7 @@ analytical oracle, so this real-data figure is corroborating. **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. -**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). +**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. A locally patched legacy build contains the same fix (`1c96b3b8e`); SIMPLNX and that patched build agree on *which* operator to apply (the residual hex hue difference between them is a separate library deviation, `-D2` below). --- @@ -54,9 +54,9 @@ analytical oracle, so this real-data figure is corroborating. | **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:** For a **hexagonal** feature, the IPF color of a basal-plane reference direction differs between SIMPLNX and legacy DREAM3D (both 6.5.171 and a locally patched legacy build): 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`. -**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`). +**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), the locally patched legacy build = `(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:** 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. diff --git a/src/Plugins/OrientationAnalysis/vv/deviations/WritePoleFigureFilter.md b/src/Plugins/OrientationAnalysis/vv/deviations/WritePoleFigureFilter.md index 81ccef1c81..f4feedc5d0 100644 --- a/src/Plugins/OrientationAnalysis/vv/deviations/WritePoleFigureFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/deviations/WritePoleFigureFilter.md @@ -4,7 +4,7 @@ This file lists every documented behavioral difference between this SIMPLNX filt 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. -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. +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, a locally patched legacy build, and SIMPLNX (EbsdLib 3.1.0). The patched legacy build's hex-Ti outputs matched 6.5.171 byte-for-byte. 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. --- diff --git a/src/Plugins/OrientationAnalysis/vv/provenance/WritePoleFigureFilter.md b/src/Plugins/OrientationAnalysis/vv/provenance/WritePoleFigureFilter.md index 3793c70369..115f633a12 100644 --- a/src/Plugins/OrientationAnalysis/vv/provenance/WritePoleFigureFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/provenance/WritePoleFigureFilter.md @@ -32,10 +32,10 @@ There is no gold-standard array in the archive. Correctness of the rendered pole - **Approving expert:** Michael Jackson (OA domain expert) - **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**: +- **What was compared:** the same orientation input rendered through DREAM3D **6.5.171**, **a locally patched legacy build**, 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/`. +- **Signed-off outputs:** the full three-way set (6.5.171 / locally patched legacy build / NX × color/discrete × hex/cubic) — hex: `out_legacy_6_5_171/{color,discrete}_Phase_1.pdf`, the patched legacy build output (matched 6.5.171 byte-for-byte), `out_nx/nx_{color,discrete}_1.png`; cubic: `out_cubic_legacy_6_5_171/`, the patched legacy build output (matched 6.5.171 except for a trailing PDF metadata block in color mode; discrete output matched byte-for-byte), `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. 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/vv/ComputeFeatureCentroidsFilter.md b/src/Plugins/SimplnxCore/vv/ComputeFeatureCentroidsFilter.md index bf0e7881cf..3cac3febea 100644 --- a/src/Plugins/SimplnxCore/vv/ComputeFeatureCentroidsFilter.md +++ b/src/Plugins/SimplnxCore/vv/ComputeFeatureCentroidsFilter.md @@ -152,7 +152,7 @@ WriteAbaqusHexahedron tests — not an orphan). ## Deviations from DREAM3D 6.5.171 -Comparison by source inspection of 6.5.171/6.5.172 `FindFeatureCentroids.cpp` vs SIMPLNX, backed by the Class 1 +Comparison by source inspection of `FindFeatureCentroids.cpp` in DREAM3D 6.5.171 and a locally patched legacy build vs SIMPLNX, backed by the Class 1 analytical fixtures that verify SIMPLNX independently. Full entries in `vv/deviations/ComputeFeatureCentroidsFilter.md`: diff --git a/src/Plugins/SimplnxCore/vv/ComputeFeatureSizesFilter.md b/src/Plugins/SimplnxCore/vv/ComputeFeatureSizesFilter.md index e38f38020c..31663d14b9 100644 --- a/src/Plugins/SimplnxCore/vv/ComputeFeatureSizesFilter.md +++ b/src/Plugins/SimplnxCore/vv/ComputeFeatureSizesFilter.md @@ -103,7 +103,7 @@ None — all fixtures constructed in C++ at test time. Provenance in `vv/provena ## 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. +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 locally patched legacy 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. **`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. diff --git a/src/Plugins/SimplnxCore/vv/FillBadDataFilter.md b/src/Plugins/SimplnxCore/vv/FillBadDataFilter.md index 3f3808e340..c82cf2da2e 100644 --- a/src/Plugins/SimplnxCore/vv/FillBadDataFilter.md +++ b/src/Plugins/SimplnxCore/vv/FillBadDataFilter.md @@ -25,7 +25,7 @@ ## Summary -`FillBadDataFilter` fills voxels with `FeatureId == 0` ("bad data") by assigning them to the most-common positive-feature neighbor. Connected regions below `minAllowedDefectSize` are filled; larger regions are preserved (optionally relabeled as a new phase). SIMPLNX uses a four-phase CCL+Union-Find architecture to support OOC datasets — a structural departure from legacy DREAM3D 6.5.171. Output equivalence is confirmed by two independent comparisons: the `FillBadData_SmallIN100` unit test (passes against `6_5_exemplar.dream3d`) and a manual A/B run on a custom dataset across three binaries (6.5.171, 6.5.172, NX). One bug surfaced during V&V (B1 — dead error-return path for `minAllowedDefectSize < 1`) has been fixed and is now covered by a dedicated test. +`FillBadDataFilter` fills voxels with `FeatureId == 0` ("bad data") by assigning them to the most-common positive-feature neighbor. Connected regions below `minAllowedDefectSize` are filled; larger regions are preserved (optionally relabeled as a new phase). SIMPLNX uses a four-phase CCL+Union-Find architecture to support OOC datasets — a structural departure from legacy DREAM3D 6.5.171. Output equivalence is supported by the `FillBadData_SmallIN100` unit test (passes against `6_5_exemplar.dream3d`). A previously cited manual A/B run on a custom dataset across DREAM3D 6.5.171, a locally patched legacy build, and DREAM3D-NX is withdrawn because its working files are unrecoverable (see deviations). One bug surfaced during V&V (B1 — dead error-return path for `minAllowedDefectSize < 1`) has been fixed and is now covered by a dedicated test. ## Algorithm Relationship diff --git a/src/Plugins/SimplnxCore/vv/ReplaceElementAttributesWithNeighborValuesFilter.md b/src/Plugins/SimplnxCore/vv/ReplaceElementAttributesWithNeighborValuesFilter.md index 0582a843e8..d1611a85b0 100644 --- a/src/Plugins/SimplnxCore/vv/ReplaceElementAttributesWithNeighborValuesFilter.md +++ b/src/Plugins/SimplnxCore/vv/ReplaceElementAttributesWithNeighborValuesFilter.md @@ -19,14 +19,14 @@ | Code paths | 16 of 17 directly tested; cancel path untestable (no cancel-signal injection in test framework). | | Tests | 2 TEST_CASEs: synthetic (4 SECTIONs, Class 1 + Class 4 oracle) + SIMPL backwards-compat (2 DYNAMIC_SECTIONs). | | Exemplar archive | `6_6_replace_element_attributes_with_neighbor.tar.gz` — unreferenced; consuming circular-oracle test removed. See `vv/provenance/ReplaceElementAttributesWithNeighborValues.md`. | -| Legacy comparison | **Complete (2026-07-21, re-run 2026-07-21 post-refactor).** Bit-identical on LessThan and GreaterThan vs 6.5.171; LessThan vs 6.5.172. No deviations. See `vv/deviations/ReplaceElementAttributesWithNeighborValuesFilter.md`. | +| Legacy comparison | **Complete (2026-07-21, re-run 2026-07-21 post-refactor).** Bit-identical on LessThan and GreaterThan vs 6.5.171; a locally patched legacy build also matched 6.5.171 bit-for-bit on LessThan. No deviations. See `vv/deviations/ReplaceElementAttributesWithNeighborValuesFilter.md`. | | Bug flags | None. | ## Summary `ReplaceElementAttributesWithNeighborValuesFilter` replaces each "bad" cell (one that fails a user-supplied threshold comparison) by copying all AttributeMatrix arrays from the best-passing face neighbor. Algorithm: (1) scan pass — for each bad voxel, check 6 face neighbors and record the one with the most-extreme passing value; (2) copy pass — apply recorded neighbor copies to all cell arrays. An optional loop repeats until no bad voxels remain. -Verification used a Class 1 (Analytical) synthetic 3×3×3 fixture with closed-form expected values across four modes (LessThan, GreaterThan, loop=false, compare1=false), paired with Class 4 (Invariant) post-loop threshold saturation and multi-array copy assertions. A/B comparison against DREAM3D 6.5.171 and 6.5.172 confirmed bit-identical output on both operators. The comparison was re-run after the algorithm was refactored and remained bit-identical. No deviations. +Verification used a Class 1 (Analytical) synthetic 3×3×3 fixture with closed-form expected values across four modes (LessThan, GreaterThan, loop=false, compare1=false), paired with Class 4 (Invariant) post-loop threshold saturation and multi-array copy assertions. A/B comparison against DREAM3D 6.5.171 confirmed bit-identical output on both operators; a locally patched legacy build also matched 6.5.171 bit-for-bit on LessThan. The comparison was re-run after the algorithm was refactored and remained bit-identical. No deviations. ## Algorithm Relationship @@ -113,6 +113,6 @@ Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReplaceEleme ## Deviations from DREAM3D 6.5.171 -No deviations. A/B comparison (2026-07-21) confirmed bit-identical output on LessThan and GreaterThan vs 6.5.171, and LessThan vs 6.5.172. +No deviations. A/B comparison (2026-07-21) confirmed bit-identical output on LessThan and GreaterThan vs 6.5.171; a locally patched legacy build also matched 6.5.171 bit-for-bit on LessThan. Full record: `vv/deviations/ReplaceElementAttributesWithNeighborValuesFilter.md` diff --git a/src/Plugins/SimplnxCore/vv/deviations/ComputeFeatureCentroidsFilter.md b/src/Plugins/SimplnxCore/vv/deviations/ComputeFeatureCentroidsFilter.md index 780c554175..35726fa3e1 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/ComputeFeatureCentroidsFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/ComputeFeatureCentroidsFilter.md @@ -4,7 +4,7 @@ This file lists every documented behavioral difference between the SIMPLNX `Comp Entries are referenced by stable ID (`ComputeFeatureCentroidsFilter-D`) from the V&V report and public migration guidance. The Filter UUID field is the permanent cross-reference anchor. -**Algorithm Relationship:** *Port.* The non-periodic path is a near line-by-line translation of `FindFeatureCentroids::find_centroids()`, preserving the identical Kahan compensated summation and `double` accumulators. Comparison basis: **source inspection of both implementations** (6.5.171/6.5.172 `FindFeatureCentroids.cpp` vs SIMPLNX `Algorithms/ComputeFeatureCentroids.cpp`), backed by the Class 1 analytical fixtures in `test/ComputeFeatureCentroidsTest.cpp` that verify SIMPLNX against hand-derived values independently of either DREAM3D version. An empirical three-way binary A/B (6.5.171 PipelineRunner + 6.5.172 backport + nxrunner) can be run to bit-confirm D1 if the SBIR deliverable requires it; source analysis is conclusive for a verified port. +**Algorithm Relationship:** *Port.* The non-periodic path is a near line-by-line translation of `FindFeatureCentroids::find_centroids()`, preserving the identical Kahan compensated summation and `double` accumulators. Comparison basis: **source inspection of both implementations** (`FindFeatureCentroids.cpp` in DREAM3D 6.5.171 and a locally patched legacy build vs SIMPLNX `Algorithms/ComputeFeatureCentroids.cpp`), backed by the Class 1 analytical fixtures in `test/ComputeFeatureCentroidsTest.cpp` that verify SIMPLNX against hand-derived values independently of either legacy build. An empirical three-way binary A/B (6.5.171 PipelineRunner + a locally patched legacy build containing the backport + nxrunner) can be run to bit-confirm D1 if the SBIR deliverable requires it; source analysis is conclusive for a verified port. --- diff --git a/src/Plugins/SimplnxCore/vv/deviations/ComputeFeatureSizesFilter.md b/src/Plugins/SimplnxCore/vv/deviations/ComputeFeatureSizesFilter.md index e8234ddd29..aabe43c946 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/ComputeFeatureSizesFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/ComputeFeatureSizesFilter.md @@ -48,7 +48,7 @@ Source-inspection comparison of `ProcessImageGeom` against DREAM3D 6.5.171 `Find **Recommendation:** Trust SIMPLNX. The legacy `findSizesUnstructured` accumulated unnecessary floating-point error that grew with feature size and grid non-uniformity. The SIMPLNX result is strictly more accurate. Users migrating pipelines should expect small positive or negative shifts in per-feature volumes. The `EquivalentDiameters` array is affected both by this volume shift **and** by a separate ESD-evaluation deviation — see `ComputeFeatureSizes-D2`. -**A/B verification (2026-06-27):** A direct comparison was run, not just source inspection. The exact RectGrid unit-test 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. Stock 6.5.171 `Volumes` differed from SIMPLNX (≈1 float32 ULP on this small fixture; grows with N). Applying **only** the float64 + Kahan summation change to legacy `findSizesUnstructured` made `Volumes` **bit-identical** to SIMPLNX, confirming summation precision as the sole root cause of the `Volumes` deviation. +**A/B verification (2026-06-27):** A direct comparison was run, not just source inspection. The exact RectGrid unit-test fixture was authored as a shared legacy `.dream3d` and run through stock DREAM3D 6.5.171, DREAM3D-NX, and a locally patched legacy build. Stock 6.5.171 `Volumes` differed from SIMPLNX (≈1 float32 ULP on this small fixture; grows with N). Applying **only** the float64 + Kahan summation change to legacy `findSizesUnstructured` made `Volumes` **bit-identical** to SIMPLNX, confirming summation precision as the sole root cause of the `Volumes` deviation. --- diff --git a/src/Plugins/SimplnxCore/vv/deviations/FillBadDataFilter.md b/src/Plugins/SimplnxCore/vv/deviations/FillBadDataFilter.md index dd8151e050..0fadedd0dd 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/FillBadDataFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/FillBadDataFilter.md @@ -8,7 +8,7 @@ Entries use stable IDs (`FillBadDataFilter-D` for legacy deviations, `FillBad The legacy comparison is the **SmallIN100 in-unit-test check**: `FillBadData_SmallIN100` compares SIMPLNX output against `6_5_exemplar.dream3d` (generated from a DREAM3D 6.5.x pipeline) with `MinAllowedDefectSize=1000`, `StoreAsNewPhase=false`. It passes — no differences. Correctness of the individual code paths is pinned independently by the Class 1 analytical fixtures (Tests 01–07, 11, 13). -> **Note — retired Test 08 A/B.** An earlier revision of this file cited a separate 7×7×3 three-way binary A/B run (DREAM3D 6.5.171 / 6.5.172 / DREAM3D-NX, `test_08_input.dream3d` + `fill_bad_data_vv/comparison_report.md`). Those working files were **not preserved and are unrecoverable**, so that comparison is **withdrawn** rather than left as an unverifiable citation. The SmallIN100 comparison above plus the analytical oracle fixtures are the standing evidence; if a second independent binary A/B is wanted for the record, it must be regenerated from scratch. +> **Note — retired Test 08 A/B.** An earlier revision of this file cited a separate 7×7×3 three-way binary A/B run (DREAM3D 6.5.171 / a locally patched legacy build / DREAM3D-NX, `test_08_input.dream3d` + `fill_bad_data_vv/comparison_report.md`). Those working files were **not preserved and are unrecoverable**, so that comparison is **withdrawn** rather than left as an unverifiable citation. The SmallIN100 comparison above plus the analytical oracle fixtures are the standing evidence; if a second independent binary A/B is wanted for the record, it must be regenerated from scratch. ## Comparison method diff --git a/src/Plugins/SimplnxCore/vv/deviations/ReplaceElementAttributesWithNeighborValuesFilter.md b/src/Plugins/SimplnxCore/vv/deviations/ReplaceElementAttributesWithNeighborValuesFilter.md index b8a3bd2aa1..5a2dbf14a5 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/ReplaceElementAttributesWithNeighborValuesFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/ReplaceElementAttributesWithNeighborValuesFilter.md @@ -15,6 +15,6 @@ A/B run confirmed **bit-identical output** between SIMPLNX and DREAM3D 6.5.171 o |-----------|------------|--------| | LessThan (index=0), threshold=0.5, loop=true | 6.5.171 vs NX | Bit-identical (125/125 voxels, ConfidenceIndex + Marker) | | GreaterThan (index=1), threshold=0.5, loop=true | 6.5.171 vs NX | Bit-identical (125/125 voxels, ConfidenceIndex + Marker) | -| LessThan, threshold=0.5, loop=true | 6.5.171 vs 6.5.172 | Bit-identical (125/125 voxels) | +| LessThan, threshold=0.5, loop=true | Locally patched legacy build vs 6.5.171 | Bit-identical (125/125 voxels) | The NX implementation is a direct port. The refactor changed internal mechanics only — output is invariant to those changes for float32 arrays. diff --git a/src/Plugins/SimplnxCore/vv/provenance/dbscan_test.md b/src/Plugins/SimplnxCore/vv/provenance/dbscan_test.md index 48ff8b8ce4..49a7d8f6f9 100644 --- a/src/Plugins/SimplnxCore/vv/provenance/dbscan_test.md +++ b/src/Plugins/SimplnxCore/vv/provenance/dbscan_test.md @@ -103,7 +103,7 @@ This archive has **not** been regenerated. Regeneration is not needed because th 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): +The historical local legacy proof build (traditional DBSCAN, via `DREAM3DReview` plugin UUID `c2d4f1e8`) was run against the same 6 sklearn datasets. Three-way comparison (the local legacy proof build vs. sklearn 1.7.1 vs. SIMPLNX): | Dataset | Legacy n_clusters | sklearn n_clusters | SIMPLNX n_clusters | Match | |---|---|---|---|---| From 98466c1229aa8905dcf00f2197c4b919257feb9f Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 20 Oct 2026 18:22:37 -0400 Subject: [PATCH 04/91] DOC: Add a part for SIMPLNX defects to the V&V templates * deviation_template.md: split the file into Part 1 (differences from DREAM3D 6.5.171) and Part 2 (SIMPLNX defects and SIMPLNX behavior changes). Part 2 entries record the affected released versions and the PR and release that fix the defect. * report_template.md: include defects that an earlier PR fixed in "Bugs found and fixed", give the lead sentence for those defects, and list only Part 1 entries in the deviations table. * report_gates.md: add the gates for "Bugs found and fixed". Signed-off-by: Michael Jackson --- docs/vv_templates/deviation_template.md | 34 ++++++++++++++++++++++--- docs/vv_templates/report_gates.md | 9 +++++++ docs/vv_templates/report_template.md | 6 +++-- 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/docs/vv_templates/deviation_template.md b/docs/vv_templates/deviation_template.md index d1e0428c6c..347b445280 100644 --- a/docs/vv_templates/deviation_template.md +++ b/docs/vv_templates/deviation_template.md @@ -1,12 +1,14 @@ # Deviations from DREAM3D 6.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 DREAM3D 6.5.171 equivalent. Part 2 lists SIMPLNX defects and SIMPLNX behavior changes that are not differences from DREAM3D 6.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. --- -## -D1 +## Part 1 — Differences from DREAM3D 6.5.171 + +### -D1 | Field | Value | |---|---| @@ -26,7 +28,7 @@ Entries are referenced by stable ID (`-D`) from the V&V report an --- -## -D2 +### -D2 | Field | Value | |---|---| @@ -44,6 +46,30 @@ 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 DREAM3D 6.5.171 was affected.* | +| **Fixed in** | *PR #, DREAM3D-NX v* *or* *this branch, the 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.* + +--- + ## Examples (delete this section before sign-off) ### Precision example diff --git a/docs/vv_templates/report_gates.md b/docs/vv_templates/report_gates.md index ae86394237..3e3485f7af 100644 --- a/docs/vv_templates/report_gates.md +++ b/docs/vv_templates/report_gates.md @@ -85,6 +85,15 @@ See [`defect_hunt.md`](./defect_hunt.md). - [ ] 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 6.5.171 was affected +- [ ] The lead sentence says which release contains each fix: the release after the latest affected release for this branch, or the named release for an earlier PR +- [ ] 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 - [ ] Every `TEST_CASE` in the filter's test file listed (including `DYNAMIC_SECTION` variants that show up as separate ctest entries — list each one) diff --git a/docs/vv_templates/report_template.md b/docs/vv_templates/report_template.md index 25e811228f..9b993a1ea8 100644 --- a/docs/vv_templates/report_template.md +++ b/docs/vv_templates/report_template.md @@ -51,10 +51,12 @@ For worked instances see `src/Plugins/OrientationAnalysis/vv/BadDataNeighborOrie ## Bugs found and fixed - + *This branch fixes all defects in this table. The fixes will be in the DREAM3D-NX release after v.* +*If an earlier PR fixed some of the defects, add:* *PR # fixed `-D`, and DREAM3D-NX v contains that fix.* + | 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.* | @@ -114,7 +116,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 | |-----------|---------------------| From ce5cd682a6cefc51b1c0b58010e53f26ca554a45 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 20 Oct 2026 18:22:37 -0400 Subject: [PATCH 05/91] DOC: Rewrite the DBSCAN V&V record to follow the report template * DBSCANFilter.md: keep only the template sections and fields. Remove the Phase 2 to Phase 13 working notes. Correct the Algorithm Relationship evidence: PR #1421 did not change the filter UUID. * Record Michael Jackson as the second engineer (2026-08-28). * Give stable IDs to the Random-seed defect (DBSCAN-D7) and the all-false-mask defect (DBSCAN-D8), and state the affected releases for D5 to D8. * Record the comparison against DREAM3D 6.5.171 (2026-10-20). It gives the same label for every point as the earlier local proof version. Put the three-way table in DBSCAN-D1 as its evidence. * deviations/DBSCANFilter.md: split into Part 1 (D1 to D3) and Part 2 (D4 to D8), and remove the incorrect note about a UUID change. * provenance/dbscan_test.md: cite the DREAM3D 6.5.171 run and record the second-engineer review. * The record describes tests and DBSCAN-D6 from PR #1766, so merge this change after that PR. Signed-off-by: Michael Jackson --- src/Plugins/SimplnxCore/vv/DBSCANFilter.md | 512 +++--------------- .../SimplnxCore/vv/deviations/DBSCANFilter.md | 102 +++- .../SimplnxCore/vv/provenance/dbscan_test.md | 25 +- 3 files changed, 190 insertions(+), 449 deletions(-) diff --git a/src/Plugins/SimplnxCore/vv/DBSCANFilter.md b/src/Plugins/SimplnxCore/vv/DBSCANFilter.md index 89dc8ed1e8..1ce327fede 100644 --- a/src/Plugins/SimplnxCore/vv/DBSCANFilter.md +++ b/src/Plugins/SimplnxCore/vv/DBSCANFilter.md @@ -1,467 +1,127 @@ # 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` | +| DREAM3D 6.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 DREAM3D 6.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 test cases in `test/DBSCANTest.cpp`, plus 1 hidden OOC test. All 15 pass in CI at `65a3038e`. | +| Code paths enumerated | 20 of 23 exercised. The 3 uncovered paths are guarded by preflight or by a forest check, 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. The six 2D label arrays are regression fixtures that the sklearn oracle confirms. The 3D label array has no external oracle. | +| Legacy comparison | Run against DREAM3D 6.5.171 on the six 2D sklearn datasets. Four datasets match exactly. Two datasets differ as `DBSCAN-D1` predicts. | +| Bug flags| `DBSCAN-D4`. Open: `upgradeParametersImpl()` is not implemented, so SIMPLNX v1 pipelines lose a non-default parse order. | +| V&V phase| Complete. 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. It uses the GDCF algorithm (Boonchoo et al. 2019), which defines core objects on grid cells. A Class 2 scikit-learn oracle verifies the filter, with Class 1 fixtures and Class 4 invariants. Four of six reference datasets match exactly, two datasets differ as designed (`DBSCAN-D1`), and the V&V found and fixed four SIMPLNX defects. ## Algorithm Relationship -*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. +*Classification:* Rewrite -*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:* PR #1421 (commit `c1bc9114b7`, 2025-09-04) replaced the point-by-point DBSCAN from PR #994 with GDCF. ## 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:* 2 (Reference — scikit-learn 1.7.1 `sklearn.cluster.DBSCAN`), with 1 (Analytical) and 4 (Invariant) companions. -**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` +*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. -*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. +*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. All registered tests pass in CI at `65a3038e`. -*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). - -*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. +This branch fixes all defects in this table. PR #1702 fixed `DBSCAN-D5`, `DBSCAN-D7` and `DBSCAN-D8`, and DREAM3D-NX v7.4.2 contains those fixes. PR #1766 fixed `DBSCAN-D6`, and the fix will be in the DREAM3D-NX release after v7.4.2. | 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. +| `DBSCAN-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 6.5.171 was not affected. | `std::shuffle` replaces the hand-written shuffle. | +| `DBSCAN-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`). | +| `DBSCAN-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 6.5.171 was not affected. | The filter gives the generated seed to the algorithm. | +| `DBSCAN-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 6.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

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/deviation_template.md b/docs/vv_templates/deviation_template.md index 347b445280..9a53e742fe 100644 --- a/docs/vv_templates/deviation_template.md +++ b/docs/vv_templates/deviation_template.md @@ -1,12 +1,12 @@ -# Deviations from DREAM3D 6.5.171: +# Deviations from DREAM.3D v6.5.171: -This file has two parts. Part 1 lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent. Part 2 lists SIMPLNX defects and SIMPLNX behavior changes that are not differences from DREAM3D 6.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. +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. Both parts use one ID sequence. The ID is stable across renames; the Filter UUID field is the permanent cross-reference anchor. --- -## Part 1 — Differences from DREAM3D 6.5.171 +## Part 1 — Differences from DREAM.3D v6.5.171 ### -D1 @@ -55,7 +55,7 @@ Entries are referenced by stable ID (`-D`) from the V&V report an | **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 DREAM3D 6.5.171 was affected.* | +| **Affected released versions** | *DREAM3D-NX v through v. State whether DREAM.3D v6.5.171 was affected.* | | **Fixed in** | *PR #, DREAM3D-NX v* *or* *this branch, the release after v* *or* *Not fixed* | **Symptom:** ** diff --git a/docs/vv_templates/oracle_classes.md b/docs/vv_templates/oracle_classes.md index 4c76aa3b22..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. @@ -217,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/report_gates.md b/docs/vv_templates/report_gates.md index 3e3485f7af..874d174bfa 100644 --- a/docs/vv_templates/report_gates.md +++ b/docs/vv_templates/report_gates.md @@ -2,13 +2,14 @@ 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 @@ -24,7 +25,7 @@ The dashboard a reviewer reads first. Lets a reviewer decide in 30 seconds wheth - [ ] **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") - [ ] **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 @@ -89,7 +90,7 @@ See [`defect_hunt.md`](./defect_hunt.md). - [ ] 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 6.5.171 was affected +- [ ] 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: the release after the latest affected release for this branch, or the named release for an earlier PR - [ ] 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 @@ -113,7 +114,7 @@ See [`defect_hunt.md`](./defect_hunt.md). - 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 9b993a1ea8..51dfe80c3e 100644 --- a/docs/vv_templates/report_template.md +++ b/docs/vv_templates/report_template.md @@ -2,13 +2,28 @@ + + | | | |--------|--------------| | 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 | +| Status | DRAFT *or* READY FOR REVIEW *or* COMPLETE | | Sign-off | ** | ## At a glance @@ -22,12 +37,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 @@ -51,7 +66,7 @@ For worked instances see `src/Plugins/OrientationAnalysis/vv/BadDataNeighborOrie ## Bugs found and fixed - + *This branch fixes all defects in this table. The fixes will be in the DREAM3D-NX release after v.* @@ -59,8 +74,8 @@ For worked instances see `src/Plugins/OrientationAnalysis/vv/BadDataNeighborOrie | 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.* | +| `-D1` | *State the defect and its effect.* | *DREAM.3D v6.5.171; DREAM3D-NX v through v.* | *State how the verified branch fixes the defect.* | +| `-D2` | *State the defect and its effect.* | *DREAM.3D v6.5.171 only. DREAM3D-NX was not affected.* | *State how the verified branch fixes or prevents the defect.* | ## Defect hunt @@ -94,7 +109,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 @@ -108,7 +123,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:* diff --git a/docs/vv_templates/vv_policy.md b/docs/vv_templates/vv_policy.md index 21ed467169..a58b5275c6 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,13 +8,13 @@ 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. From c98c6de751b8fe00be9aeb29722359e208d020bb Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 20 Oct 2026 19:33:24 -0400 Subject: [PATCH 08/91] DOC: Apply the report conventions to the DBSCAN V&V record * Add the SIMPLNX Human Name header row. * Write the legacy application as "DREAM.3D v6.5.171" or "DREAM.3D". Signed-off-by: Michael Jackson --- src/Plugins/SimplnxCore/vv/DBSCANFilter.md | 17 +++++------ .../SimplnxCore/vv/deviations/DBSCANFilter.md | 28 +++++++++---------- .../SimplnxCore/vv/provenance/dbscan_test.md | 8 +++--- 3 files changed, 27 insertions(+), 26 deletions(-) diff --git a/src/Plugins/SimplnxCore/vv/DBSCANFilter.md b/src/Plugins/SimplnxCore/vv/DBSCANFilter.md index 1ce327fede..121a708b23 100644 --- a/src/Plugins/SimplnxCore/vv/DBSCANFilter.md +++ b/src/Plugins/SimplnxCore/vv/DBSCANFilter.md @@ -6,7 +6,8 @@ |--------|--------------| | Plugin | SimplnxCore | | SIMPLNX UUID | `763dad44-fad7-4606-808f-617867257b98` | -| DREAM3D 6.5.171 equivalent | `DBSCAN` (DREAM3DReview plugin, SIMPL UUID `c2d4f1e8-2b04-5d82-b90f-2191e8f4262e`) | +| 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* | @@ -15,12 +16,12 @@ | Aspect | Current state| |----------|--------------| -| Algorithm Relationship | Rewrite. The legacy equivalent is `DBSCAN` in DREAM3D 6.5.171. SIMPLNX uses grid-based DBSCAN (GDCF), so a core object is a grid cell, not a point. | +| 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 test cases in `test/DBSCANTest.cpp`, plus 1 hidden OOC test. All 15 pass in CI at `65a3038e`. | | Code paths enumerated | 20 of 23 exercised. The 3 uncovered paths are guarded by preflight or by a forest check, 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. The six 2D label arrays are regression fixtures that the sklearn oracle confirms. The 3D label array has no external oracle. | -| Legacy comparison | Run against DREAM3D 6.5.171 on the six 2D sklearn datasets. Four datasets match exactly. Two datasets differ as `DBSCAN-D1` predicts. | +| Legacy comparison | Run against DREAM.3D v6.5.171 on the six 2D sklearn datasets. Four datasets match exactly. Two datasets differ as `DBSCAN-D1` predicts. | | Bug flags| `DBSCAN-D4`. Open: `upgradeParametersImpl()` is not implemented, so SIMPLNX v1 pipelines lose a non-default parse order. | | V&V phase| Complete. The OOC test suite was not run, because CI does not build the OOC configuration. | @@ -50,10 +51,10 @@ This branch fixes all defects in this table. PR #1702 fixed `DBSCAN-D5`, `DBSCAN | Deviation | Defect | Affected released versions | Resolution in this branch | |-----------|--------|----------------------------|---------------------------| -| `DBSCAN-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 6.5.171 was not affected. | `std::shuffle` replaces the hand-written shuffle. | +| `DBSCAN-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. | | `DBSCAN-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`). | -| `DBSCAN-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 6.5.171 was not affected. | The filter gives the generated seed to the algorithm. | -| `DBSCAN-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 6.5.171 was not affected. | An explicit check returns warning `-85640` with a message about the mask. | +| `DBSCAN-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. | +| `DBSCAN-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 @@ -116,9 +117,9 @@ The phases are (p) preflight, (a) grid construction, (b) core identification, (c - **SHA512:** `77d7886e2550b63176b564e827d7de320b5a28b1c8a55bf107d53acd6962757275bc86b3382ff789f612a6838f55ab8f4af29435aec83a7a804b9487e57a6386` - **Provenance:** `src/Plugins/SimplnxCore/vv/provenance/dbscan_test.md` -## Deviations from DREAM.3D 6.5.171 +## Deviations from DREAM.3D v6.5.171 -See `vv/deviations/DBSCANFilter.md` for the root cause, affected users, and recommendation for each deviation. DREAM3D 6.5.171 ran on the six 2D sklearn datasets in `dbscan_test.tar.gz`. +See `vv/deviations/DBSCANFilter.md` for the root cause, affected users, and recommendation for each deviation. DREAM.3D v6.5.171 ran on the six 2D sklearn datasets in `dbscan_test.tar.gz`. | Deviation | Observed difference | |-----------|---------------------| diff --git a/src/Plugins/SimplnxCore/vv/deviations/DBSCANFilter.md b/src/Plugins/SimplnxCore/vv/deviations/DBSCANFilter.md index fd1af2aacc..716df27783 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/DBSCANFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/DBSCANFilter.md @@ -1,12 +1,12 @@ -# Deviations from DREAM3D 6.5.171: DBSCANFilter +# Deviations from DREAM.3D v6.5.171: DBSCANFilter -This file has two parts. Part 1 lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent (D1–D3). Part 2 lists SIMPLNX defects and SIMPLNX behavior changes that are not differences from DREAM3D 6.5.171 (D4–D8). +This file has two parts. Part 1 lists every documented behavioral difference between this SIMPLNX filter and its DREAM.3D v6.5.171 equivalent (D1–D3). Part 2 lists SIMPLNX defects and SIMPLNX behavior changes that are not differences from DREAM.3D v6.5.171 (D4–D8). Entries are referenced by stable ID (`DBSCAN-D`) from the V&V report and from public migration guidance. The Filter UUID fields are the permanent cross-reference anchors. --- -## Part 1 — Differences from DREAM3D 6.5.171 +## Part 1 — Differences from DREAM.3D v6.5.171 ### DBSCAN-D1 @@ -19,9 +19,9 @@ Entries are referenced by stable ID (`DBSCAN-D`) from the V&V report and from **Symptom:** Sparse datasets where individual data points each have ≥ minPoints neighbors within ε (and would form clusters under traditional DBSCAN) may produce **more noise (cluster 0) points in SIMPLNX** if those neighborhoods span multiple grid cells with fewer than minPoints points per cell. -**Evidence (DREAM3D 6.5.171, 2026-10-20):** DREAM3D 6.5.171, scikit-learn 1.7.1 and SIMPLNX ran on the six sklearn datasets in `dbscan_test.tar.gz`. The table gives the cluster count, the noise count and the sorted cluster sizes. An earlier run on a local proof version (2026-08-05) gave the same label for every point as DREAM3D 6.5.171. Evidence: `dbscan_vv/ab_6_5_171_comparison_results.json`. +**Evidence (DREAM.3D v6.5.171, 2026-10-20):** DREAM.3D v6.5.171, scikit-learn 1.7.1 and SIMPLNX ran on the six sklearn datasets in `dbscan_test.tar.gz`. The table gives the cluster count, the noise count and the sorted cluster sizes. An earlier run on a local proof version (2026-08-05) gave the same label for every point as DREAM.3D v6.5.171. Evidence: `dbscan_vv/ab_6_5_171_comparison_results.json`. -| Dataset | ε | minPts | DREAM3D 6.5.171 | scikit-learn 1.7.1 | SIMPLNX | +| Dataset | ε | minPts | DREAM.3D v6.5.171 | scikit-learn 1.7.1 | SIMPLNX | |---|---|---|---|---|---| | 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] | @@ -30,9 +30,9 @@ Entries are referenced by stable ID (`DBSCAN-D`) from the V&V report and from | 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] | -Four datasets match exactly in all three. For `ansio` and `varied`, DREAM3D 6.5.171 and scikit-learn find the same number of clusters, and SIMPLNX finds 3. SIMPLNX does not find the micro-clusters of 6 points or fewer. Its large clusters match the large clusters of DREAM3D 6.5.171 exactly for `varied` and within 6 points for `ansio`. +Four datasets match exactly in all three. For `ansio` and `varied`, DREAM.3D v6.5.171 and scikit-learn find the same number of clusters, and SIMPLNX finds 3. SIMPLNX does not find the micro-clusters of 6 points or fewer. Its large clusters match the large clusters of DREAM.3D v6.5.171 exactly for `varied` and within 6 points for `ansio`. -DREAM3D 6.5.171 and scikit-learn differ by a small number of border points. DREAM3D 6.5.171 uses `dist < epsilon`, and scikit-learn uses `dist <= epsilon`. The DREAM3D 6.5.171 input is float32, and the scikit-learn input is float64. The `ansio` cluster of size 1 in DREAM3D 6.5.171 comes from these differences. It does not change this deviation. +DREAM.3D v6.5.171 and scikit-learn differ by a small number of border points. DREAM.3D v6.5.171 uses `dist < epsilon`, and scikit-learn uses `dist <= epsilon`. The DREAM.3D v6.5.171 input is float32, and the scikit-learn input is float64. The `ansio` cluster of size 1 in DREAM.3D v6.5.171 comes from these differences. It does not change this deviation. **Root cause:** Algorithmic choice. SIMPLNX implements Grid-based DBSCAN (GDCF, Boonchoo et al. 2019). The core-object definition differs fundamentally: - **Legacy (traditional DBSCAN)**: A data point `p` is a *core point* if the ε-ball centered on `p` contains ≥ minPoints data points (inclusive of `p` itself). Cluster membership is point-centric. @@ -62,9 +62,9 @@ Users with dense, well-clustered data (many points per grid cell) are unlikely t | **Legacy SIMPL UUID** | `c2d4f1e8-2b04-5d82-b90f-2191e8f4262e` | | **Status** | active | -**Symptom:** When run with default parameters on data where multiple clusters exist, SIMPLNX typically assigns cluster IDs in a different order than legacy DREAM3D. The cluster membership (which points belong together) is functionally identical, but the numeric labels differ. +**Symptom:** When run with default parameters on data where multiple clusters exist, SIMPLNX typically assigns cluster IDs in a different order than legacy DREAM.3D. The cluster membership (which points belong together) is functionally identical, but the numeric labels differ. -**Root cause:** Algorithmic choice. SIMPLNX adds `LowDensityFirst` parse order (the new default) with no equivalent in legacy DREAM3D. `LowDensityFirst` sorts core grids ascending by occupancy (less dense grids processed first) before the union-find merge phase, which changes the order in which cluster IDs are assigned. Legacy DREAM3D used an effectively arbitrary (memory-layout-dependent) order for core point processing. +**Root cause:** Algorithmic choice. SIMPLNX adds `LowDensityFirst` parse order (the new default) with no equivalent in legacy DREAM.3D. `LowDensityFirst` sorts core grids ascending by occupancy (less dense grids processed first) before the union-find merge phase, which changes the order in which cluster IDs are assigned. Legacy DREAM.3D used an effectively arbitrary (memory-layout-dependent) order for core point processing. The `Random` parse order in SIMPLNX uses a time-based seed (non-deterministic), so even successive identical runs may produce different cluster ID numbering. `SeededRandom` provides reproducibility with an explicit user seed. @@ -103,7 +103,7 @@ The `Random` parse order in SIMPLNX uses a time-based seed (non-deterministic), | **SIMPLNX UUID** | `763dad44-fad7-4606-808f-617867257b98` | | **Legacy SIMPL UUID** | `c2d4f1e8-2b04-5d82-b90f-2191e8f4262e` | | **Status** | active | -| **Affected released versions** | DREAM3D-NX v7.2.2 through v7.4.2. DREAM3D 6.5.171 was not affected. | +| **Affected released versions** | DREAM3D-NX v7.2.2 through v7.4.2. DREAM.3D v6.5.171 was not affected. | | **Fixed in** | Not fixed | **Symptom:** SIMPLNX pipelines saved prior to PR #1421 that contain the parameter key `init_type_index` will silently lose their parse-order setting when loaded in SIMPLNX v2. The filter loads without error, but the parse order silently falls back to `LowDensityFirst` (the default). SIMPL (6.4/6.5) pipelines are **not** affected — SIMPL never had an `init_type_index` parameter. @@ -126,7 +126,7 @@ The `FromSIMPLJson` converter (`DBSCANFilter.cpp`, starting at line 242) is not | **SIMPLNX UUID** | `763dad44-fad7-4606-808f-617867257b98` | | **Legacy SIMPL UUID** | `c2d4f1e8-2b04-5d82-b90f-2191e8f4262e` | | **Status** | active | -| **Affected released versions** | DREAM3D-NX v7.2.2 through v7.4.1. DREAM3D 6.5.171 was not affected. | +| **Affected released versions** | DREAM3D-NX v7.2.2 through v7.4.1. DREAM.3D v6.5.171 was not affected. | | **Fixed in** | PR #1702, DREAM3D-NX v7.4.2 | **Symptom:** For a given `SeededRandom` seed value, SIMPLNX now produces a different core-grid processing order — and therefore potentially different cluster-ID assignments — compared to earlier SIMPLNX builds (prior to this PR). Cluster *membership* (which points are grouped together) is unaffected for well-separated datasets; only cluster *ID numbering* and boundary-point assignment at cluster edges may differ. @@ -152,7 +152,7 @@ The `FromSIMPLJson` converter (`DBSCANFilter.cpp`, starting at line 242) is not **Symptom:** With **Use Mask Array** enabled, SIMPLNX previously accepted a multi-component or short **Cell Mask Array** in preflight. The Direct path read the mask by flat element, while the external Scanline path rejected it during execution with `-54062`. Both paths now reject these inputs before execution: component shape other than `{1}` produces `-208`, and unequal tuple counts produce `-7587`. -**Root cause:** The mask selection parameter did not restrict component shape, and preflight did not compare the tuple counts of **Cell Mask Array** and **Attribute Array to Cluster**. Validation now applies before algorithm dispatch. This is a within-SIMPLNX correction; no new comparison with DREAM3D 6.5.171 is claimed. +**Root cause:** The mask selection parameter did not restrict component shape, and preflight did not compare the tuple counts of **Cell Mask Array** and **Attribute Array to Cluster**. Validation now applies before algorithm dispatch. This is a within-SIMPLNX correction; no new comparison with DREAM.3D v6.5.171 is claimed. **Affected users:** Users whose pipelines select a multi-component mask or a mask with a different number of tuples from the clustering DataArray. Previously accepted Direct executions may have used unintended mask elements or accessed beyond a short mask. @@ -168,7 +168,7 @@ The `FromSIMPLJson` converter (`DBSCANFilter.cpp`, starting at line 242) is not | **SIMPLNX UUID** | `763dad44-fad7-4606-808f-617867257b98` | | **Legacy SIMPL UUID** | `c2d4f1e8-2b04-5d82-b90f-2191e8f4262e` | | **Status** | active | -| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM3D 6.5.171 was not affected. | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | | **Fixed in** | PR #1702, DREAM3D-NX v7.4.2 | **Symptom:** In DREAM3D-NX v7.0.0 through v7.4.1, the `Random` parse order gave the same result on every run. It gave the same result as `SeededRandom` with the default seed. @@ -189,7 +189,7 @@ The `FromSIMPLJson` converter (`DBSCANFilter.cpp`, starting at line 242) is not | **SIMPLNX UUID** | `763dad44-fad7-4606-808f-617867257b98` | | **Legacy SIMPL UUID** | `c2d4f1e8-2b04-5d82-b90f-2191e8f4262e` | | **Status** | active | -| **Affected released versions** | DREAM3D-NX v7.2.2 through v7.4.1. DREAM3D 6.5.171 was not affected. | +| **Affected released versions** | DREAM3D-NX v7.2.2 through v7.4.1. DREAM.3D v6.5.171 was not affected. | | **Fixed in** | PR #1702, DREAM3D-NX v7.4.2 | **Symptom:** None observed. In DREAM3D-NX v7.2.2 through v7.4.1, a mask with no true values gave the correct warning `-85640`, but only by chance. diff --git a/src/Plugins/SimplnxCore/vv/provenance/dbscan_test.md b/src/Plugins/SimplnxCore/vv/provenance/dbscan_test.md index a98e217ef2..416204d37d 100644 --- a/src/Plugins/SimplnxCore/vv/provenance/dbscan_test.md +++ b/src/Plugins/SimplnxCore/vv/provenance/dbscan_test.md @@ -75,7 +75,7 @@ These are the Class 2 oracle expected values. SIMPLNX cluster counts and sizes s ### What was done to resolve it 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 the DREAM3D 6.5.171 comparison. +2. ✅ **SIMPLNX verified against oracle** (Phase 6) — 4/6 exact match; 2 deviations explained by DBSCAN-D1 and corroborated by the DREAM.3D v6.5.171 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. @@ -97,12 +97,12 @@ The circular-oracle situation identified in Phase 2 was resolved in Phase 6 reco This archive has **not** been regenerated. Regeneration is not needed because the independent oracle confirmed the existing arrays are correct. -## DREAM3D 6.5.171 comparison (2026-10-20) +## DREAM.3D v6.5.171 comparison (2026-10-20) Script: `dbscan_vv/ab_6_5_171.py` Results: `dbscan_vv/ab_6_5_171_comparison_results.json` -The DREAM3D 6.5.171 `PipelineRunner` ran the `DBSCAN` filter (DREAM3DReview plugin, UUID `c2d4f1e8`) on the same 6 sklearn datasets. An earlier run on a local proof version (2026-08-05) gave the same label for every point. Three-way comparison (DREAM3D 6.5.171 vs. sklearn 1.7.1 vs. SIMPLNX): +The DREAM.3D v6.5.171 `PipelineRunner` ran the `DBSCAN` filter (DREAM3DReview plugin, UUID `c2d4f1e8`) on the same 6 sklearn datasets. An earlier run on a local proof version (2026-08-05) gave the same label for every point. Three-way comparison (DREAM.3D v6.5.171 vs. sklearn 1.7.1 vs. SIMPLNX): | Dataset | 6.5.171 n_clusters | sklearn n_clusters | SIMPLNX n_clusters | Match | |---|---|---|---|---| @@ -113,4 +113,4 @@ The DREAM3D 6.5.171 `PipelineRunner` ran the `DBSCAN` filter (DREAM3DReview plug | no_structure | 1 | 1 | 1 | Exact three-way match | | varied | 11 | 11 | 3 | 6.5.171 = sklearn; SIMPLNX differs (DBSCAN-D1) | -**Conclusion**: The regression fixtures in this archive are consistent with both the Class 2 sklearn oracle (confirmed Phase 6) and the DREAM3D 6.5.171 comparison. 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. +**Conclusion**: The regression fixtures in this archive are consistent with both the Class 2 sklearn oracle (confirmed Phase 6) and the DREAM.3D v6.5.171 comparison. 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. From 15225bb51df1e1c3756e7bc08996d5b1788d817b Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 20 Oct 2026 19:52:17 -0400 Subject: [PATCH 09/91] DOC: Rewrite the ErodeDilateMask V&V record to follow the report template * Keep only the template sections and fields, and describe the current rolling-slice algorithm. * Map 26 code paths to the 13 current test cases. * State the release ranges of D1 and D2 from the release branches and the DREAM.3D v6.5.171 source. * Record the 28 of 28 match against DREAM.3D v6.5.171 (2026-08-18). * Make the provenance sidecar follow the provenance template. Signed-off-by: Michael Jackson --- .../SimplnxCore/vv/ErodeDilateMaskFilter.md | 405 ++++-------------- .../vv/deviations/ErodeDilateMaskFilter.md | 108 ++--- .../vv/provenance/6_6_erode_dilate_test.md | 89 ++-- 3 files changed, 157 insertions(+), 445 deletions(-) diff --git a/src/Plugins/SimplnxCore/vv/ErodeDilateMaskFilter.md b/src/Plugins/SimplnxCore/vv/ErodeDilateMaskFilter.md index 1f0c20732e..6e3aa936cc 100644 --- a/src/Plugins/SimplnxCore/vv/ErodeDilateMaskFilter.md +++ b/src/Plugins/SimplnxCore/vv/ErodeDilateMaskFilter.md @@ -1,361 +1,124 @@ # V&V Report: ErodeDilateMaskFilter -| | | -|-----------------------------|--------------------------------------------------------------------------| -| Plugin | SimplnxCore | -| SIMPLNX UUID | `cab66cd1-f64c-42b4-8f94-18f0835a967f` | -| SIMPLNX Human Name | Erode/Dilate Mask | -| DREAM3D 6.5.171 equivalent | `ErodeDilateMask` — SIMPL UUID `4fff1aa6-4f62-56c4-8ee9-8e28ec2fcbba` | -| Verified commit | ** | + + +| | | +|--------|--------------| +| Plugin | SimplnxCore | +| SIMPLNX UUID | `cab66cd1-f64c-42b4-8f94-18f0835a967f` | +| SIMPLNX Human Name | Erode/Dilate Mask | +| DREAM.3D v6.5.171 equivalent | `ErodeDilateMask` (Processing plugin, SIMPL UUID `4fff1aa6-4f62-56c4-8ee9-8e28ec2fcbba`) | +| Verified commit | ** | | Status | READY FOR REVIEW | -| Sign-off | *delegated to the PR reviewer (requester decision 2026-08-19)* | +| Sign-off | *delegated to the PR reviewer (requester decision 2026-08-19)* | ## At a glance -| Aspect | Current state | -|------------------------|----------------| -| Algorithm Relationship | **Port** — legacy `ErodeDilateMask.cpp` was read against `Algorithms/ErodeDilateMask.cpp` this pass. Neighbour offsets, boundary validity, the double-buffered `maskCopy` scan, and both operation branches are structurally identical. Two divergences were found and **both are now fixed**: the direction flags were not honoured, and the legacy `NumIterations <= 0` preflight guard was absent (`ErodeDilateMaskFilter-D1`, resolved 2026-08-19 by restoring the guard per the requester's product decision). | -| Oracle (confirmed) | **Class 1 (Analytical).** Expected mask contents were derived by hand from the face-neighbour contract on a 5×5×1 single-seed grid (centre seed and boundary seed) and a 3×3×3 hollow cube, written into the test before the filter was run. 7 `TEST_CASE`s (the erode direction-flag case carries two `SECTION`s), all pass. The five single-iteration hand-derived index sets were independently reproduced by the DREAM3D 6.5.171 binary in the A/B, so the oracle is confirmed against legacy as well. | -| Code paths enumerated | **10 of 10 exercised.** The multi-iteration loop (`NumIterations > 1`) is covered by a 2-iteration dilate oracle, and the `NumIterations <= 0` preflight rejection added on 2026-08-19 is covered by a negative test; no enumerated path is untested. | -| Tests today | **7 oracle `TEST_CASE`s + 1 validation `TEST_CASE` + 1 SIMPL backwards-compatibility `TEST_CASE`, 315 assertions, all pass.** Coverage shape: dilate and erode at all-directions-on, one direction-gated case per axis that the fixtures can discriminate (X off and Y off on the 5×5×1 dilate, Z off on the 3×3×3 erode), a 2-iteration dilate, a boundary-seeded dilate, and a `NumIterations` of `0` / `-2` rejected at preflight. | -| Exemplar archive | **None consumed any more.** The two 6.6-derived exemplar tests that read `6_6_erode_dilate_test.tar.gz` were retired and replaced by the inline Class 1 oracles. The archive itself stays in `test/CMakeLists.txt` because `ErodeDilateBadDataTest` and `ErodeDilateCoordinationNumberTest` still consume it. | -| Legacy comparison | **Run.** 14 combinations ({dilate, erode} × 7 non-empty direction combinations, 1 iteration) executed on 2 fixtures — 28 pipeline pairs, 56 binary runs — through DREAM3D 6.5.171 `PipelineRunner` and `nxrunner` against a shared legacy-format input. **28/28 exact element-wise matches** on the `Mask` array. | -| Bug flags | Two port omissions found and fixed this pass — direction flags ignored, and the missing `NumIterations <= 0` preflight guard. Both restore behaviour 6.5.171 already had, so **neither leaves an active deviation**; see [Bug Fixes](#bug-fixes-this-pass). `ErodeDilateMaskFilter-D1` is retained as a **retired** entry documenting the historical difference. **No active deviations.** | -| V&V phase | Oracle chosen, derived by hand, and confirmed; direction-flag regression reproduced by failing test then fixed; binary A/B run and green; multi-iteration, per-axis and boundary gaps closed; `ErodeDilateMaskFilter-D1` resolved 2026-08-19 by restoring the legacy preflight guard, reproduced by a failing test first. Outstanding before promotion to COMPLETE: PR-reviewer sign-off (see below). | - -## Outstanding before COMPLETE - -- **Second-engineer sign-off on the oracle design** — *delegated to the PR reviewer (requester decision - 2026-08-19, per the program plan's standing practice).* -- **Out-of-core build runs** — *waived by the requester 2026-08-19.* Verification was run in the in-core - build (`NX-Com-Qt69-Vtk96-Rel`) only. -- `ErodeDilateMaskFilter-D1` — **closed 2026-08-19** (requester product decision: match legacy; guard - restored and covered by a negative test). No longer outstanding. +| Aspect | Current state | +|--------|---------------| +| Algorithm Relationship | Port of `ErodeDilateMask` in DREAM.3D v6.5.171. SIMPLNX uses rolling slice buffers for the same synchronous face-neighbour operations. | +| Oracle (confirmed) | Class 1 analytical masks are encoded in 8 test cases in `test/ErodeDilateMaskTest.cpp`. All pass in CI at `01809c236`. | +| Code paths enumerated | 18 of 26 exercised by the mapped test cases. All-directions-off, six I/O failure paths and external-store execution lack direct tests in this file. | +| Tests today | 13 test cases: 8 analytical, 2 exemplar comparisons, 1 validation, 1 SIMPL conversion and 1 hidden data generator. | +| Exemplar archive | `6_6_erode_dilate_test.tar.gz` supplies inputs and expected outputs for the two exemplar comparisons. It does not establish parity with DREAM.3D v6.5.171. | +| Legacy comparison | Run against DREAM.3D v6.5.171 on 2026-08-18: 28 of 28 mask cases match on a 5×5×1 centre seed and a 3×3×3 hollow cube. | +| Bug flags | None. | +| V&V phase | Analytical fixtures and regressions for both fixed defects are encoded. All registered tests pass in CI at `01809c236`. PR-reviewer sign-off remains outstanding. | ## Summary -`ErodeDilateMaskFilter` grows (dilate) or shrinks (erode) the `true` region of a boolean cell mask on -an Image Geometry by one voxel per iteration across the six face neighbours, optionally restricted to -any non-empty combination of the X, Y, and Z axes. Verification is **Class 1 analytical**: expected -mask contents were derived by hand from the algorithm's face-neighbour contract on 5×5×1 single-seed -grids and a 3×3×3 hollow cube, written into the test first, and only then run. One SIMPLNX-side -defect was found and fixed — the three direction parameters were never read by the algorithm, a **port -regression** carrying **no deviation entry** (see [Bug Fixes](#bug-fixes-this-pass)) — after which the -binary A/B shows parity on all 28 pipeline pairs. A second port omission, the missing -`NumIterations <= 0` preflight guard, was documented as `ErodeDilateMaskFilter-D1` and left open for a -product decision; the requester decided on **2026-08-19 to match legacy**, so the guard was restored -under TDD and D1 is now **retired**. All 9 test cases pass. +`ErodeDilateMaskFilter` grows or shrinks a boolean cell mask by one face-neighbour layer per iteration on the selected axes. Class 1 fixtures check exact masks, and two archive tests compare reconstruction data with stored exemplars. The branch fixes ignored direction flags and acceptance of non-positive iteration counts. All registered tests pass in CI at `01809c236`, and all 28 legacy comparison cases match. ## Algorithm Relationship -*Classification:* **Port** ~~| Minor changes | Rewrite | New filter~~ - -*Evidence:* `SimplnxCoreLegacyUUIDMapping.hpp:234` maps legacy SIMPL UUID -`4fff1aa6-4f62-56c4-8ee9-8e28ec2fcbba` directly to `FilterTraits`, and -`test/simpl_conversion/{6_4,6_5}/ErodeDilateMaskFilter.json` carry the legacy -`Direction`/`NumIterations`/`XDirOn`/`YDirOn`/`ZDirOn`/`MaskArrayPath` parameter set unchanged — the -same filter with the same parameter model. The legacy source -(`Source/Plugins/Processing/ProcessingFilters/ErodeDilateMask.cpp`, from a sibling `DREAM3D` checkout -on the authoring engineer's machine, not committed to this repository) was read against -`Algorithms/ErodeDilateMask.cpp` this pass rather than inferred from documentation. - -*Port-time deltas:* - -1. **Face-neighbour offsets** — legacy computes `neighpoints[6]` inline; SIMPLNX calls - `initializeFaceNeighborOffsets(dims)` from `NeighborUtilities`. Same six offsets in the same - `[-Z,-Y,-X,+X,+Y,+Z]` order (legacy's `l == 0..5` maps one-for-one onto - `VoxelNeighbors::k_NegativeZNeighbor .. k_PositiveZNeighbor`). No output change. -2. **Boundary-validity checks** — legacy folds the boundary test into the same `good = 0` chain that - carries the direction flags; SIMPLNX calls `computeValidFaceNeighbors(x, y, z, dims)` and then - masks the result. Reproduces the same six conditions. No output change. -3. **Direction gating** — legacy ORs the direction flag into each per-face check - (`if(l == 0 && (k == 0 || !m_ZDirOn))`); SIMPLNX masks the per-voxel `isValidFaceNeighbor` array in - `adjustValidNeighbors`. Equivalent once wired in — but it was *not* wired in at all, which is the - port regression fixed by this pass. -4. **`NumIterations` validation** — legacy's `dataCheck()` errors with `-5555` when - `NumIterations <= 0`; SIMPLNX had no equivalent preflight guard and silently no-op'd. Verified by - running both binaries. Recorded as `ErodeDilateMaskFilter-D1` and initially left unchanged, because - adding a preflight error is a behavioural change to a shipping filter and belongs to a product - decision rather than a V&V pass. **The requester decided on 2026-08-19 to match legacy**, and the - guard is now in `preflightImpl` under error code `-14701`; see - [Bug Fixes](#bug-fixes-this-pass). No output change; parity restored on validation behaviour. -5. **`maskCopy` storage** — legacy allocates a `BoolArrayType` named `_INTERNAL_USE_ONLY_MaskCopy` - and initializes it to `false`; SIMPLNX uses a `std::vector`. Both are overwritten wholesale - from `mask` at the top of every iteration before being read, so the initial value is immaterial. - No output change. -6. **Progress reporting** — SIMPLNX emits a per-iteration `Info` message through the message handler; - legacy used `notifyStatusMessage`. No output change. - -*Material PRs since baseline:* `Algorithms/ErodeDilateMask.cpp` has been touched by the -`NeighborUtilities` factoring (deltas 1–2, which introduced the `isValidFaceNeighbor` array that -delta 3 masks) and by store-API/rename churn with no behavioural content. The direction-flag -regression is a consequence of delta 3 never being completed for this filter — the same omission that -`ErodeDilateBadData` carried until PR #1687. - -*SIMPLNX implementation:* `Algorithms/ErodeDilateMask.cpp` (123 lines). Single-threaded; no parallel -algorithm, no cancel check. - -## Bug Fixes (this pass) - -### Direction parameters had no effect — fixed, port regression, no deviation entry - -The `X Direction` / `Y Direction` / `Z Direction` parameters were declared, presented in the UI, and -converted from SIMPL pipelines, and they reached the algorithm's input-values struct intact — -`ErodeDilateMaskFilter.cpp:59-61` declares the three parameters, `:107-109` copies them in, and -`ErodeDilateMask.hpp:29-31` declares the fields. `Algorithms/ErodeDilateMask.cpp` then read -`m_InputValues->Operation`, `NumIterations`, `MaskArrayPath`, and `InputImageGeometry`, and never read -`XDirOn`, `YDirOn`, or `ZDirOn`. Every voxel visited all six in-bounds face neighbours regardless of -the flags, so all three parameters silently did nothing. - -Reproduced first, by two tests written before the fix: - -- `Class 1 dilate honours direction flags` — 5×5×1, single true seed, `XDirOn = false`. Expected the - vertical 3-cell bar `{7, 12, 17}`; pre-fix the filter produced the full plus shape, failing at - index 11. -- `Class 1 erode honours direction flags` — 3×3×3 hollow cube, `ZDirOn = false`. Expected the ±Z - neighbours of the centre (indices 4 and 22) to survive; pre-fix they were cleared, failing at - index 4. - -A third case, `Class 1 dilate with Y direction off`, was added afterwards to pin the remaining axis: -with only X and Z gating asserted, a partial reintroduction of the regression that drops or -mis-targets the Y flag would go unnoticed. Confirmed discriminating — with the `&& yDir` term removed -from `adjustValidNeighbors`, that case is the only one in the suite that fails. - -Fixed by adding a file-local `adjustValidNeighbors(isValidFaceNeighbor, xDir, yDir, zDir)` that ANDs -each of the six entries against its axis flag using the named `VoxelNeighbors` constants, and -calling it at `Algorithms/ErodeDilateMask.cpp:93` immediately after `computeValidFaceNeighbors(...)`, -alongside — not instead of — the boundary check. The axis mapping was **copied verbatim** from -`Algorithms/ErodeDilateBadData.cpp:64-80`, per the branch-history note on -`ErodeDilateBadDataFilter-D1`: an earlier attempt at that fix bitwise-ANDed face indices against -booleans and swapped axes, and re-deriving the mapping is exactly how that was reintroduced. - -The helper was kept file-local rather than hoisted into `NeighborUtilities.hpp` to share with -`ErodeDilateBadData`. Rationale: hoisting touches a core header consumed by filters outside the scope -of this V&V, and the duplication is 8 lines. If a reviewer prefers de-duplication it is a mechanical -follow-up. - -DREAM3D 6.5.171 honours the flags (`ErodeDilateMask.cpp:227-247` gates each of the six neighbour -indices on `m_XDirOn` / `m_YDirOn` / `m_ZDirOn`), so this is a **port regression, not a deviation from -legacy behaviour**. Per the bug-adjudication protocol that makes it Verdict B, second branch: it gets -**no deviation entry**, because once fixed no difference from legacy remains. It is described here and -summarised in the Summary instead. With the fix in place all test cases pass and the post-fix binary -A/B shows parity on all 28 pipeline pairs. - -### `NumIterations <= 0` accepted silently — fixed, port omission, legacy-shared behaviour restored - -`Number of Iterations` is an unconstrained `Int32Parameter`, and `ErodeDilateMaskFilter::preflightImpl` -validated nothing — it only called `MarkDataPathModified`. The algorithm's -`for(int32 iteration = 0; iteration < m_InputValues->NumIterations; iteration++)` loop therefore never -executed for a value below 1, and the filter reported success while writing the mask out bit-identical -to the input. DREAM3D 6.5.171's `ErodeDilateMask::dataCheck()` rejected exactly that configuration with -error `-5555`, so this is a **port omission** of a guard legacy already had, not a SIMPLNX design -choice. +*Classification:* Port -The V&V pass as first written recorded it as `ErodeDilateMaskFilter-D1` and deliberately did not change -the filter: adding a preflight error is a behavioural change to a shipping filter and is a product -decision. **The requester made that decision on 2026-08-19 — match legacy, add the preflight error** — -on the grounds that it restores parity, has zero migration impact (a `NumIterations <= 0` pipeline -already failed in legacy, so no working legacy pipeline can depend on the SIMPLNX no-op), and requires -no parameter-version bump because no parameter key changes. `parametersVersion()` accordingly stays -at `1`. - -Reproduced first, under TDD: `SimplnxCore::ErodeDilateMaskFilter: Invalid Number of Iterations` was -written and run **before** the guard existed and failed on both `GENERATE`d values — -`REQUIRE((preflightResult.outputActions).invalid())` with expansion `false` at `numIterations := 0` and -again at `numIterations := -2` — confirming the pre-fix silent-accept. The guard was then added to -`ErodeDilateMaskFilter.cpp::preflightImpl`: - -- Condition `NumIterations < 1`, returning `MakeErrorResult(k_InvalidNumIterationsError, ...)`. -- Error code **`-14701`**, declared as a file-local `constexpr int32` in an anonymous namespace, per the - house convention used by the sibling `ErodeDilateBadDataFilter` (`k_NoDirectionsError = -14601`, - `k_NoGeometryDimensionsError = -14602`). `-147xx` is the first free block adjacent to that family; - legacy's `-5555` was deliberately **not** reused: it belongs to the legacy code space, and `-5555` is - already taken in SIMPLNX for unrelated conditions (`RequireMinNumNeighborsFilter`'s - unavailable-phase execute error, `ApplyTransformationToGeometry`'s unused-cell-matrix warning). -- The message carries the offending value and the affected array, per the error-messages house rule: - `Number of Iterations (-2) must be at least 1. Erode/Dilate Mask performs one erode or dilate pass per iteration, so a value less than 1 would leave the mask array '' unmodified.` - -With the guard in place the test passes (13 assertions across the two generated values) and the other -8 test cases are unaffected — no existing test configures a non-positive iteration count. +*Evidence:* `src/Plugins/SimplnxCore/src/SimplnxCore/SimplnxCoreLegacyUUIDMapping.hpp` maps the legacy UUID to this filter; the DREAM.3D v6.5.171 source uses the same six-neighbour rules, while PR #1568 changes storage access to rolling slice buffers. ## Oracle -*Class:* **1 (Analytical)** — closed-form expected output on hand-built input. - -*Applied:* `ErodeDilateMask::operator()` double-buffers. It copies `mask` into `maskCopy`, walks every -voxel, and for voxels where `!mask[voxelIndex]` inspects the six face neighbours. Dilate sets -`maskCopy[voxelIndex] = true` when a face neighbour is true (the false voxel joins the region); erode -sets `maskCopy[neighpoint] = false` (the true *neighbour* leaves the region). The asymmetry is -deliberate and both formulations were checked against the legacy source before being used as the -oracle basis. Because only `mask` is read during a pass and only `maskCopy` is written, one iteration -is a pure function of the input grid and can be evaluated by hand. - -Three fixtures: +*Class:* 1 (Analytical). -- **5×5×1, single true voxel at (2, 2) — flat index 12.** Dilate/all-on: the four false voxels - touching the seed flip, giving the plus shape `{7, 11, 12, 13, 17}`; the grid is one plane thick so - `computeValidFaceNeighbors` reports ±Z invalid everywhere and no Z growth is possible. Erode/all-on: - each of those four false voxels sees the seed and clears it, giving `{}`. Dilate with - `XDirOn = false`: (1,2) and (3,2) can no longer reach the seed, leaving the vertical bar - `{7, 12, 17}`. Dilate with `YDirOn = false`: the mirror case, leaving the horizontal bar - `{11, 12, 13}`. Dilate with `NumIterations = 2`: pass 1 gives the plus, and pass 2 — derived cell by - cell over all 20 remaining false voxels against pass 1's written-back mask — gives the 13-cell - diamond `{2, 6, 7, 8, 10, 11, 12, 13, 14, 16, 17, 18, 22}`, every cell within Manhattan distance 2 - of the seed that fits on the grid. -- **5×5×1, single true voxel on the x = 0 face at (0, 2) — flat index 10.** Dilate/all-on gives - `{5, 10, 11, 15}`. This fixture exists because a *centred* seed cannot detect a swap of the −X and - +X entries of `neighborVoxelIndexOffsets`: `computeValidFaceNeighbors` gates by coordinate, not by - offset, so for an interior seed both X gates are open on both neighbours and a swap merely re-labels - which entry reaches the seed — the reached set `{11, 13}` is unchanged. An off-centre but still - interior seed does not help either, since a plus shape is mirror-symmetric about its own centre - wherever that centre sits. Seating the seed against the face breaks the symmetry through the *gate*: - under a swapped ±X pair, voxel 9 = (4, 1) — whose −X gate is open because 4 > 0 — would read offset - +1, land on index 10, and grow a cell that wrapped around the row end. Index 9 is therefore the - discriminator, and it is also the only case in the suite where the seed's own −X neighbour is - rejected by the boundary check. -- **3×3×3, true everywhere except the centre (1,1,1) — flat index 13.** The centre is the only false - voxel and therefore the only voxel that enters the neighbour loop; its six face neighbours are - `-Z = 4`, `-Y = 10`, `-X = 12`, `+X = 14`, `+Y = 16`, `+Z = 22`. Erode/all-on clears all six - (20 voxels stay true). Erode with `ZDirOn = false` clears only four, so 4 and 22 survive - (22 voxels stay true). This fixture exists because the 5×5×1 grid cannot discriminate the Z flag. +*Applied:* Hand-derived true-index sets and four-cell literal arrays specify synchronous dilation, erosion, boundary clipping and axis restrictions. -Every expected index set is spelled out in a derivation comment in the test immediately above the -assertion, and each assertion checks the **exact** expected true-index set element-wise over the whole -grid, not a count or a spot check. +*Encoded:* `src/Plugins/SimplnxCore/test/ErodeDilateMaskTest.cpp` — seven `Class 1` cases use three fixture layouts; `SimplnxCore::ErodeDilateMaskFilter: Literal One-Dimensional Morphology` adds two axis layouts and ten sections with exact mask and unchanged-sibling checks. All pass in CI at `01809c236`. -*Independent confirmation:* the five single-iteration index sets were extracted from the DREAM3D -6.5.171 binary's A/B output by `ww_work/ErodeDilateMask/oracle_check.py` and match exactly. The oracle -is therefore confirmed by two sources that are independent of SIMPLNX's own behaviour: hand -derivation, and the legacy implementation. The 2-iteration and boundary-seed sets rest on hand -derivation alone (the A/B sweep runs `NumIterations = 1` on the centred fixtures). +*Second-engineer review:* *delegated to the PR reviewer (requester decision 2026-08-19); sign-off pending.* -*Encoded:* +## Bugs found and fixed -- `src/Plugins/SimplnxCore/test/ErodeDilateMaskTest.cpp::SimplnxCore::ErodeDilateMaskFilter: Class 1 dilate, one iteration` — 34 assertions. -- `...::Class 1 erode, one iteration` — 34 assertions. -- `...::Class 1 dilate honours direction flags` — 34 assertions. -- `...::Class 1 dilate with Y direction off` — 34 assertions. -- `...::Class 1 dilate, two iterations` — 34 assertions. -- `...::Class 1 dilate from a boundary seed` — 34 assertions. -- `...::Class 1 erode honours direction flags` — two `SECTION`s (`All directions on`, `Z direction off`), 71 assertions. +This branch fixes all defects in this table. The fixes will be in the DREAM3D-NX release after v7.4.2. PR #1568 fixed `ErodeDilateMaskFilter-D2`, and PR #1717 fixed `ErodeDilateMaskFilter-D1`. -7 `TEST_CASE`s, 275 assertions, all pass. - -*Discrimination evidence:* the three tests added in this pass were each checked against the specific -single-line algorithm mutation they exist to catch, by temporarily applying the mutation, rebuilding -`SimplnxCoreUnitTest`, and re-running the suite. Dropping `&& yDir` from `adjustValidNeighbors` fails -`Class 1 dilate with Y direction off` **and nothing else**. Hoisting the per-iteration write-back -below the iteration loop fails `Class 1 dilate, two iterations` **and nothing else**. Swapping the −X -and +X entries of `neighborVoxelIndexOffsets` fails `Class 1 dilate from a boundary seed` at index 9 — -exactly the predicted discriminator — plus the 2-iteration case, and none of the four pre-existing -oracle tests. The mutations were reverted and the suite re-confirmed green before commit. - -One mutation from the review list proved to be an **equivalent mutant** rather than a coverage gap: -hoisting the per-iteration `maskCopy[j] = mask[j]` seeding *above* the loop changes no output and -cannot be killed by any test. Because the write-back at the end of each iteration already leaves -`mask == maskCopy`, re-seeding at the top of the next iteration is the identity, and both operations -write `maskCopy` monotonically (dilate only sets `true`, erode only sets `false`). The in-loop seeding -is therefore redundant given the in-loop write-back; only the write-back carries the inter-iteration -dataflow, and that is what `Class 1 dilate, two iterations` pins. - -*Second-engineer review:* **delegated to the PR reviewer** (requester decision 2026-08-19, per the -program plan's standing practice). Sign-off on the oracle design happens in PR review rather than as a -separate named review. +| Deviation | Defect | Affected released versions | Resolution in this branch | +|-----------|--------|----------------------------|---------------------------| +| `ErodeDilateMaskFilter-D1` | A non-positive iteration count returned success without changing the mask. | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was not affected. | Preflight rejects values below 1 with error `-14701`. | +| `ErodeDilateMaskFilter-D2` | Disabled X, Y or Z directions still participated in morphology. | DREAM3D-NX v7.4.1 through v7.4.2. DREAM.3D v6.5.171 was not affected. | The algorithm applies each direction flag to its two face neighbours. | ## Code path coverage -10 of 10 paths exercised. The algorithm has two logical phases: (a) per-iteration setup and write-back, -and (b) the per-voxel face-neighbour scan; phase (c) is the filter's preflight validation. +18 of 26 paths exercised. The uncovered rows identify the remaining gaps. + +Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ErodeDilateMask.cpp` (247 lines), `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ErodeDilateMaskFilter.cpp` (164 lines), and `src/simplnx/Utilities/NeighborUtilities.hpp` (634 lines). -Sources: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ErodeDilateMask.cpp` (123 lines) -for phases (a) and (b), and `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ErodeDilateMaskFilter.cpp` -`preflightImpl` for phase (c). +Test names below omit the common `SimplnxCore::ErodeDilateMaskFilter` prefix. The phases are setup, scan, I/O and preflight. | # | Phase | Path | Test case | |---|-------|------|-----------| -| 1 | (a) Setup | `maskCopy` allocation, dims conversion, face-offset and internal-index initialization, per-iteration `maskCopy[j] = mask[j]` seeding | All 7 oracle tests | -| 2 | (a) Write-back | `mask[j] = maskCopy[j]` at the end of each iteration | All 7 oracle tests (every assertion reads the written-back array). `Class 1 dilate, two iterations` additionally pins the write-back's *position inside* the loop: hoisting it below the loop fails that test and no other. | -| 3 | (a) Iteration loop | `NumIterations > 1` — a second pass over the already-updated mask | `Class 1 dilate, two iterations` — 5×5×1 centre seed, all flags on, `NumIterations = 2`, asserting the 13-cell diamond derived cell by cell from pass 1's written-back mask. This is the only test that reads the inter-iteration dataflow; every other oracle case, and the whole binary A/B, runs a single iteration. | -| 4 | (b) Per-voxel | `mask[voxelIndex] == true` → voxel skipped, neighbours never inspected | All 7 oracle tests (24 of 25 cells in the 5×5×1 dilate case, 26 of 27 in the hollow cube) | -| 5 | (b) Neighbour gate | `!isValidFaceNeighbor[faceIndex]` from the **boundary** check → `continue` | All 7 oracle tests (±Z always invalid on the 5×5×1 grid; every hollow-cube face voxel loses at least one neighbour). `Class 1 dilate from a boundary seed` is the only case where the boundary check fires on the **seed's own** missing neighbour, and the only one whose result changes if the ±X offsets are swapped (it fails at index 9). | -| 6 | (b) Neighbour gate | `!isValidFaceNeighbor[faceIndex]` from the **direction** mask in `adjustValidNeighbors` → `continue` | Per axis: `Class 1 dilate honours direction flags` (X masked), `Class 1 dilate with Y direction off` (Y masked), `Class 1 erode honours direction flags` / `Z direction off` (Z masked). The X and Z cases failed before the fix and pass after it; the Y case was added afterwards and is the sole failure when the `yDir` term is removed. | -| 7 | (b) Dilate | `Operation == k_DilateIndex && mask[neighpoint]` → `maskCopy[voxelIndex] = true` | `Class 1 dilate, one iteration`, `Class 1 dilate honours direction flags`, `Class 1 dilate with Y direction off`, `Class 1 dilate, two iterations`, `Class 1 dilate from a boundary seed` | -| 8 | (b) Erode | `Operation == k_ErodeIndex && mask[neighpoint]` → `maskCopy[neighpoint] = false` | `Class 1 erode, one iteration`, `Class 1 erode honours direction flags` (both sections) | -| 9 | (b) Neighbour false | valid neighbour with `mask[neighpoint] == false` → neither branch fires, nothing written | All 7 oracle tests (in the 5×5×1 dilate case most valid neighbour pairs are false-false) | -| 10 | (c) Preflight validation | `NumIterations < 1` → `MakeErrorResult(-14701, ...)`, added 2026-08-19 | `Class 1 dilate, one iteration` and the other 6 oracle tests take the accepting branch (`NumIterations` of 1 or 2). `Invalid Number of Iterations` takes the rejecting branch, `GENERATE`ing `0` (the boundary) and `-2` (negative) and asserting both `outputActions.invalid()` and `errors()[0].code == -14701`. Written before the guard and confirmed failing on both values. | - -Observations that are **not** deviations: - -- **No cancel check.** `m_ShouldCancel` is stored and exposed via `getCancel()` but never polled in - the voxel loop. Legacy has no cancel check either, so SIMPLNX is at parity; it is a - responsiveness gap on very large volumes, not a correctness one, and is out of scope for this pass. -- **Preflight error paths.** `preflightImpl` calls `MarkDataPathModified` and, as of 2026-08-19, - validates `NumIterations >= 1` (`-14701`). The geometry and mask are selection parameters and are - validated by the parameter system, so they need no explicit check. With the `NumIterations` guard - restored, SIMPLNX's preflight validation now matches 6.5.171's `dataCheck()`. +| 1 | Setup | Allocate read/write slice buffers and seed both from the mask | `Class 1 dilate, one iteration` | +| 2 | I/O | Write the completed mask back within each iteration | `Class 1 dilate, two iterations` | +| 3 | Setup | Read the previous iteration's output for a second pass | `Class 1 dilate, two iterations`; `Literal One-Dimensional Morphology` / `Dilate two iterations` | +| 4 | Scan | A true voxel skips the neighbour scan | `Class 1 dilate, one iteration`; `Class 1 erode honours direction flags` | +| 5 | Scan | Reject an out-of-bounds face neighbour | `Class 1 dilate from a boundary seed`; `Literal One-Dimensional Morphology` | +| 6 | Scan | Disable X neighbours | `Class 1 dilate honours direction flags`; `Literal One-Dimensional Morphology` / `X` / `Disabled active axis erosion` | +| 7 | Scan | Dilate a false voxel that has a true enabled neighbour | `Class 1 dilate, one iteration`; `Literal One-Dimensional Morphology` / `Z` / `Dilate one iteration` | +| 8 | Scan | Erode a true neighbour of a false voxel, within or across slices | `Class 1 erode, one iteration`; `Class 1 erode honours direction flags` / `All directions on` | +| 9 | Scan | A false neighbour causes no write | `Class 1 dilate, one iteration` | +| 10 | Preflight | Reject `NumIterations < 1` with `-14701` | `Invalid Number of Iterations` — values 0 and -2 | +| 11 | Scan | Disable Y neighbours | `Class 1 dilate with Y direction off` | +| 12 | Scan | Disable Z neighbours | `Class 1 erode honours direction flags` / `Z direction off`; `Literal One-Dimensional Morphology` / `Z` / `Disabled active axis` | +| 13 | Scan | Disable all three directions; preserve the mask | *Not directly tested. No case sets all three flags to false.* | +| 14 | Setup | Read the second slice when `dims[2] > 1` | `Class 1 erode honours direction flags` | +| 15 | Scan | Rotate both windows; read another slice when available and skip that read at the last slice | `Literal One-Dimensional Morphology` / `Z` | +| 16 | I/O | Write the completed previous slice when `zIdx > 0` | `Class 1 erode honours direction flags` | +| 17 | I/O | Flush a single-slice image | `Class 1 dilate, one iteration` | +| 18 | I/O | Flush the last slice of a multi-slice image | `Class 1 erode honours direction flags`; `Literal One-Dimensional Morphology` / `Z` | +| 19 | Preflight | Accept a positive iteration count and mark the mask modified | `Class 1 dilate, one iteration` — preflight succeeds; execution checks mask values | +| 20 | I/O | Propagate failure while reading the first slice | *Not directly tested. Requires an injected store read failure.* | +| 21 | I/O | Propagate failure while reading the second initial slice | *Not directly tested. Requires an injected store read failure.* | +| 22 | I/O | Propagate failure while reading a slice after window rotation | *Not directly tested. Requires an injected store read failure.* | +| 23 | I/O | Propagate failure while writing a completed previous slice | *Not directly tested. Requires an injected store write failure.* | +| 24 | I/O | Propagate failure while flushing a single-slice image | *Not directly tested. Requires an injected store write failure.* | +| 25 | I/O | Propagate failure while flushing the last multi-slice image slice | *Not directly tested. Requires an injected store write failure.* | +| 26 | I/O | Read and write a mask held in an external store | *Not directly tested. No case in this file requires an external mask store.* | ## Test inventory | Test case | Status | Notes | |-----------|--------|-------| -| `SimplnxCore::ErodeDilateMaskFilter(Dilate)` | retired | 6.6-derived exemplar comparison against `Exemplar Mask Dilate` in `6_6_erode_dilate_mask.dream3d`. Retired: the exemplar was generated with all three directions on, so it could not discriminate direction gating — a build with the regression still present passed it. Replaced by the Class 1 oracles. | -| `SimplnxCore::ErodeDilateMaskFilter(Erode)` | retired | Same, for `Exemplar Mask Erode`. Same reason. | -| `SimplnxCore::ErodeDilateMaskFilter: Class 1 dilate, one iteration` | new-for-V&V | Class 1 oracle. 5×5×1 single seed, all directions on, 1 iteration. Asserts the exact expected true-index set element-wise over all 25 cells; 34 assertions. | -| `SimplnxCore::ErodeDilateMaskFilter: Class 1 erode, one iteration` | new-for-V&V | Class 1 oracle. Same fixture, erode. Asserts the mask goes entirely false; 34 assertions. | -| `SimplnxCore::ErodeDilateMaskFilter: Class 1 dilate honours direction flags` | new-for-V&V | Class 1 oracle, direction-gated. 5×5×1, `XDirOn = false`, dilate. Asserts the vertical bar `{7, 12, 17}`; 34 assertions. Written before the fix and confirmed failing at index 11 with the regression present. | -| `SimplnxCore::ErodeDilateMaskFilter: Class 1 dilate with Y direction off` | new-for-V&V | Class 1 oracle, direction-gated. 5×5×1 centre seed, `YDirOn = false`, dilate. Asserts the horizontal bar `{11, 12, 13}`; 34 assertions. Pins the Y flag specifically: removing the `&& yDir` term from `adjustValidNeighbors` fails this test and no other. | -| `SimplnxCore::ErodeDilateMaskFilter: Class 1 dilate, two iterations` | new-for-V&V | Class 1 oracle, multi-iteration. 5×5×1 centre seed, all flags on, `NumIterations = 2`. Asserts the 13-cell diamond `{2, 6, 7, 8, 10, 11, 12, 13, 14, 16, 17, 18, 22}`, derived cell by cell over all 20 false voxels of pass 1's output; 34 assertions. Closes coverage path 3 and pins the write-back's position inside the iteration loop. | -| `SimplnxCore::ErodeDilateMaskFilter: Class 1 dilate from a boundary seed` | new-for-V&V | Class 1 oracle, boundary. 5×5×1 with the seed on the x = 0 face at (0, 2), dilate, all flags on. Asserts `{5, 10, 11, 15}`; 34 assertions. The only case that discriminates a −X/+X offset swap (fails at index 9, the row-wrap cell) and the only one exercising the boundary rejection on the seed's own neighbour. | -| `SimplnxCore::ErodeDilateMaskFilter: Class 1 erode honours direction flags` | new-for-V&V | Class 1 oracle, direction-gated. 3×3×3 hollow cube, erode, `SECTION`s for `ZDirOn = true` (20 cells true) and `ZDirOn = false` (22 cells true, ±Z neighbours survive); 71 assertions. Written before the fix and confirmed failing at index 4 with the regression present. | -| `SimplnxCore::ErodeDilateMaskFilter: Invalid Number of Iterations` | new-for-V&V | Validation test (not an oracle test). `GENERATE`s `NumIterations` of `0` and `-2` on the 5×5×1 centre-seed fixture, preflights, and asserts `SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions)` plus `errors()[0].code == -14701`; 13 assertions. Closes coverage path 10. Written before the guard existed and confirmed failing on both generated values. Pins the resolution of `ErodeDilateMaskFilter-D1`. | -| `SimplnxCore::ErodeDilateMaskFilter: SIMPL Backwards Compatibility` | kept | Byte-identical to the pre-V&V version. `DYNAMIC_SECTION` over `simpl_conversion/6_5/ErodeDilateMaskFilter.json` (matched by `Filter_Uuid`) and `simpl_conversion/6_4/ErodeDilateMaskFilter.json` (matched by `Filter_Name`). Confirms a single `PipelineFilter` with the right UUID and checks the converted arguments (`Operation == 0`, `NumIterations == 5`, all three direction flags `true`, geometry `DataPath({"DataContainer"})`, mask `DataPath({"DataContainer","CellData","TestArray"})`); 27 assertions. Not an oracle test. | - -All 9 remaining test cases pass in the in-core build (`NX-Com-Qt69-Vtk96-Rel`), 315 assertions total -(275 across the 7 oracle cases, 13 in the validation case, 27 in the backwards-compatibility case). -Out-of-core build runs were waived by the requester on 2026-08-19. +| `SimplnxCore::ErodeDilateMaskFilter: Generate Test Data` | kept | Hidden (`[.GenerateTestData]`). Writes 20×20×20 and 200×200×200 patterned masks; does not execute morphology or assert its output. | +| `SimplnxCore::ErodeDilateMaskFilter(Dilate)` | kept | Runs two iterations with all axes enabled and in-memory storage. Compares cell arrays with `Exemplar Mask Dilate` and checks tuple dimensions. | +| `SimplnxCore::ErodeDilateMaskFilter(Erode)` | kept | Runs two iterations with all axes enabled and in-memory storage. Compares cell arrays with `Exemplar Mask Erode` and checks tuple dimensions. | +| `SimplnxCore::ErodeDilateMaskFilter: Literal One-Dimensional Morphology` | kept | Checks exact four-cell masks and unchanged sibling values on X and Z lines. Each axis has `Dilate one iteration`, `Dilate two iterations`, `Erode one iteration`, `Disabled active axis` and `Disabled active axis erosion` sections. | +| `SimplnxCore::ErodeDilateMaskFilter: Class 1 dilate, one iteration` | new-for-V&V | On a 5×5×1 centre seed, checks all 25 cells against `{7, 11, 12, 13, 17}`. | +| `SimplnxCore::ErodeDilateMaskFilter: Class 1 erode, one iteration` | new-for-V&V | On the centre seed, checks that all 25 cells become false. | +| `SimplnxCore::ErodeDilateMaskFilter: Class 1 dilate honours direction flags` | new-for-V&V | Disables X and checks the vertical bar `{7, 12, 17}`. Covers `ErodeDilateMaskFilter-D2`. | +| `SimplnxCore::ErodeDilateMaskFilter: Class 1 dilate with Y direction off` | new-for-V&V | Disables Y and checks the horizontal bar `{11, 12, 13}`. Covers `ErodeDilateMaskFilter-D2`. | +| `SimplnxCore::ErodeDilateMaskFilter: Class 1 dilate, two iterations` | new-for-V&V | Checks the exact 13-cell diamond after two synchronous passes on the centre seed. | +| `SimplnxCore::ErodeDilateMaskFilter: Class 1 dilate from a boundary seed` | new-for-V&V | Seeds the x = 0 face and checks `{5, 10, 11, 15}` across all 25 cells, including the false row-wrap cell at index 9. | +| `SimplnxCore::ErodeDilateMaskFilter: Class 1 erode honours direction flags` | new-for-V&V | Checks all 27 hollow-cube cells in `All directions on` and `Z direction off`: 20 and 22 cells remain true. Covers `ErodeDilateMaskFilter-D2`. | +| `SimplnxCore::ErodeDilateMaskFilter: Invalid Number of Iterations` | new-for-V&V | Checks invalid preflight and error `-14701` for 0 and -2. Covers `ErodeDilateMaskFilter-D1`. | +| `SimplnxCore::ErodeDilateMaskFilter: SIMPL Backwards Compatibility` | kept | `SIMPL 6.5 (UUID)` and `SIMPL 6.4 (Filter_Name)` sections check the converted UUID and all arguments; they do not execute the filter. | ## Exemplar archive -- **Archive:** none consumed by this filter's tests any more. -- **SHA512:** n/a -- **Provenance:** n/a - -The retired tests read `6_6_erode_dilate_mask.dream3d` out of `6_6_erode_dilate_test.tar.gz`. That -archive is **not** removed from `src/Plugins/SimplnxCore/test/CMakeLists.txt:225` by this PR: -`ErodeDilateBadDataTest.cpp:357` and `ErodeDilateCoordinationNumberTest.cpp:38` still consume it. -Retirement of the archive completes in whichever PR lands last among the erode/dilate conversions, -per the plan's grep guard. - -The A/B artifacts (legacy-format inputs, 56 pipelines, per-combination outputs, diff scripts, ReadMe) -live in `/Users/mjackson/Workspace9/ww_work/ErodeDilateMask/` and are uploaded to OneDrive; they are -never committed. - -## Deviations from DREAM3D 6.5.171 - -The comparison was run as a full binary A/B: 14 combinations ({dilate, erode} × the 7 non-empty -direction combinations, `NumIterations = 1`) on 2 fixtures — the 5×5×1 single-seed grid and the -3×3×3 hollow cube the unit tests build — giving 28 pipeline pairs and 56 binary runs through -`PipelineRunner` (6.5.171) and `nxrunner` against a single shared legacy-format input per fixture. - -**28/28 combinations match element-wise on the `Mask` array. No A/B difference was observed.** -Per-combination results and the re-run recipe are in -`/Users/mjackson/Workspace9/ww_work/ErodeDilateMask/ReadMe.md` (`results_compare.txt`). - -Two of the 28 (`5x5x1_dilate_Z`, `5x5x1_erode_Z`) are no-ops on both sides — a one-plane-thick grid -has no Z face neighbours — and are reported as such by the diff script rather than being quietly -counted as evidence. The 3×3×3 fixture carries the Z-direction evidence. +- **Archive:** `6_6_erode_dilate_test.tar.gz` +- **SHA512:** `5f0773e5d296936effbb2239965f5847e7c18533b0a2c3ec6a1d6a83b03417e5b459cce29808c8e0273613b3b6fa032c675e84926eb35d8da8a6ddc0641a0ef5` +- **Provenance:** `src/Plugins/SimplnxCore/vv/provenance/6_6_erode_dilate_test.md` -The direction-flag regression fixed by this pass produces **no deviation entry**: 6.5.171 was correct -(`ErodeDilateMask.cpp:227-247`), SIMPLNX was wrong, and once fixed there is no remaining difference. -Comparing the legacy outputs against what pre-fix SIMPLNX would have produced (the all-directions -result for every flag setting) shows the A/B would have failed on 12 of the 28 combinations. +## Deviations from DREAM.3D v6.5.171 -**No deviations remain open.** One was recorded during this pass, found by reading the legacy -`dataCheck()` and then confirmed by running both binaries, and has since been resolved: +See `vv/deviations/ErodeDilateMaskFilter.md` for the root cause, affected users, and recommendation. This table lists only Part 1; the fixed direction-flag defect is in Part 2. -- `ErodeDilateMaskFilter-D1` — `NumIterations <= 0` was a preflight error in 6.5.171 and a silent - no-op in SIMPLNX. **Retired 2026-08-19:** on the requester's product decision to match legacy, the - preflight guard was restored (error `-14701`, message carrying the offending value) under TDD, so no - difference remains. The entry is retained in - [`deviations/ErodeDilateMaskFilter.md`](deviations/ErodeDilateMaskFilter.md) with `Status: retired` - because it documents a real historical difference between the shipped versions. +| Deviation | Observed difference | +|-----------|---------------------| +| `ErodeDilateMaskFilter-D1` | Historical difference, retired 2026-08-19: SIMPLNX accepted non-positive iteration counts, while DREAM.3D v6.5.171 rejected them. Both now reject these values; the error codes differ (`-14701` and `-5555`). | diff --git a/src/Plugins/SimplnxCore/vv/deviations/ErodeDilateMaskFilter.md b/src/Plugins/SimplnxCore/vv/deviations/ErodeDilateMaskFilter.md index aba4c5a6ac..ac2574777e 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/ErodeDilateMaskFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/ErodeDilateMaskFilter.md @@ -1,89 +1,57 @@ -# Deviations from DREAM3D 6.5.171: ErodeDilateMaskFilter +# Deviations from DREAM.3D v6.5.171: ErodeDilateMaskFilter -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 records the historical validation difference from DREAM.3D v6.5.171 (D1). Part 2 records the fixed SIMPLNX direction-flag defect (D2). Both fixes are present in the current branch. -Entries are referenced by stable ID (`ErodeDilateMaskFilter-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. - -**Output parity:** the binary A/B run for this V&V — 14 combinations ({dilate, erode} × the 7 non-empty -direction combinations, `NumIterations = 1`) on 2 fixtures, 28 pipeline pairs — produced **28/28 -element-wise matches** on the `Mask` array. **No output-value deviation was observed.** The one entry -below is a validation-behaviour difference, not an output difference, and it has since been -**resolved** — as of 2026-08-19 there are **no active deviations** for this filter. +Entries use stable IDs (`ErodeDilateMaskFilter-D`) from one sequence. The Filter UUID is the permanent cross-reference anchor. D1 keeps its original ID and retirement date; that date records the V&V decision, not a released fix. --- -## ErodeDilateMaskFilter-D1 +## Part 1 — Differences from DREAM.3D v6.5.171 + +### ErodeDilateMaskFilter-D1 | Field | Value | |---|---| | **Deviation ID** | `ErodeDilateMaskFilter-D1` | | **Filter UUID** | `cab66cd1-f64c-42b4-8f94-18f0835a967f` | -| **Status** | retired 2026-08-19 — difference eliminated in SIMPLNX by restoring the legacy guard (requester product decision, 2026-08-19). Retained for the historical record. | - -**Symptom (as observed during this V&V, before the fix):** A pipeline that sets `Number of Iterations` -to zero or a negative value fails preflight in DREAM3D 6.5.171 with -`Error (-5555): The number of iterations (0) must be positive`, but ran to completion in SIMPLNX and -wrote the mask out unchanged. A user migrating such a pipeline got a silently unchanged mask where -they previously got a hard stop. - -**Root cause:** Algorithmic choice — a validation guard that was not carried across in the port. -Legacy `ErodeDilateMask::dataCheck()` (`Source/Plugins/Processing/ProcessingFilters/ErodeDilateMask.cpp`, -in the `dataCheck` body) sets error condition `-5555` when `getNumIterations() <= 0`. SIMPLNX's -`ErodeDilateMaskFilter::preflightImpl` performs no equivalent check — it only calls -`MarkDataPathModified` — and `k_NumIterations_Key` is an unconstrained `Int32Parameter`. The -algorithm's `for(int32 iteration = 0; iteration < m_InputValues->NumIterations; iteration++)` loop -then simply never executes, so the filter succeeds and the mask is untouched. Confirmed by running -both binaries on the same input with `NumIterations = 0` -(`ww_work/ErodeDilateMask/extra/`, "Extra probe" section of that folder's `ReadMe.md`). - -**Affected users:** Anyone who reaches `NumIterations <= 0`, which in practice means pipelines built -by scripting or templating where the iteration count is computed rather than typed. Interactively the -value is rarely zero. The consequence is a silent no-op rather than wrong data: the mask that comes -out is bit-identical to the mask that went in, so downstream results are those of "filter not run", -not those of a corrupted run. - -**Recommendation:** Trust SIMPLNX — **no remaining difference.** The V&V pass as first written left -this open, because adding a preflight error is a behavioural change to a shipping filter and therefore -a product decision rather than a verification finding. The requester made that decision on -**2026-08-19: match legacy, add the preflight error.** Rationale recorded with the decision — it -restores 6.5.171 parity, has zero migration impact (a `NumIterations <= 0` pipeline already errored in -legacy, so no working legacy pipeline can be relying on the SIMPLNX no-op), and needs no -parameter-version bump because no parameter key changes. - -**Resolution (2026-08-19):** `ErodeDilateMaskFilter::preflightImpl` now returns -`MakeErrorResult(k_InvalidNumIterationsError /* -14701 */, ...)` when -`NumIterations < 1`, with the offending value in the message. SIMPLNX's error code is **-14701**, not -legacy's `-5555`: `-5555` belongs to the legacy code space, and the SIMPLNX house convention is a -file-local `constexpr int32 k_...Error` in an anonymous namespace drawn from a per-filter block — -`-147xx` is the first free block adjacent to the sibling `ErodeDilateBadDataFilter`'s `-146xx` -(`k_NoDirectionsError = -14601`, `k_NoGeometryDimensionsError = -14602`). Covered by -`SimplnxCore::ErodeDilateMaskFilter: Invalid Number of Iterations`, which preflights `NumIterations` -of `0` and `-2` and asserts both the invalid result and the code. +| **Status** | retired 2026-08-19 | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | PR #1717 (`ba246bc7c`), the DREAM3D-NX release after v7.4.2. | + +**Symptom:** SIMPLNX accepted a zero or negative iteration count and returned the input mask unchanged; DREAM.3D v6.5.171 rejected the same setting. + +**Root cause:** Bug. The port omitted the positive-iteration check from `ErodeDilateMask::dataCheck()`. The algorithm loop did not run when `NumIterations < 1`. Current `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ErodeDilateMaskFilter.cpp:94-107` rejects these values with `-14701` and includes the value and mask path in the message. DREAM.3D v6.5.171 source, tag `v6.5.171`, `Source/Plugins/Processing/ProcessingFilters/ErodeDilateMask.cpp:137-142`, uses `-5555` for the same condition. + +**Affected users:** Users whose scripts or saved pipelines supply an iteration count below 1. Earlier SIMPLNX execution silently skipped morphology. + +**Recommendation:** Trust SIMPLNX on this branch. Supply at least one iteration; the filter now reports invalid counts before execution. The error code differs from DREAM.3D v6.5.171, but both reject the input. + +**Evidence:** `SimplnxCore::ErodeDilateMaskFilter: Invalid Number of Iterations` checks invalid preflight and error `-14701` for 0 and -2. The requester chose the legacy validation behavior on 2026-08-19; no parameter key changed, and `parametersVersion()` remains 1. The zero-iteration comparison is recorded in `vv_work:ErodeDilateMaskFilter/ReadMe.md`, with inputs in `vv_work:ErodeDilateMaskFilter/extra/legacy_numiter0.json` and `vv_work:ErodeDilateMaskFilter/extra/nx_numiter0.d3dpipeline`. The test passes in CI at `01809c236`. All release branches from `upstream/release/v7.0.0` through `upstream/release/v7.4.2` omit the guard in `ErodeDilateMaskFilter.cpp`. --- -## Confirmed non-deviations +## Part 2 — SIMPLNX defects and behavior changes + +### ErodeDilateMaskFilter-D2 + +| Field | Value | +|---|---| +| **Deviation ID** | `ErodeDilateMaskFilter-D2` | +| **Filter UUID** | `cab66cd1-f64c-42b4-8f94-18f0835a967f` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.4.1 through v7.4.2. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | PR #1568 (`4ec21e93f`), the DREAM3D-NX release after v7.4.2; PR #1717 (`ba246bc7c`) moves the gating into the shared helper. | + +**Symptom:** Disabling X, Y or Z still allowed growth or erosion along that axis. -Recorded so they are not relitigated. +**Root cause:** Bug. PR #1523 (`9bfcb01e4`, 2026-02-05) replaced the explicit face checks with neighbour utilities but omitted direction gating. Its parent reads all three flags; the changed algorithm reads none. This was a refactor regression, not an omission in the original port. PR #1568 restores explicit per-axis gates. Current `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ErodeDilateMask.cpp:156` calls `adjustValidNeighbors`; `src/simplnx/Utilities/NeighborUtilities.hpp:329-337` applies each flag to the two matching faces. The DREAM.3D v6.5.171 source gates all six neighbours on the corresponding axis flags. -### Direction parameters ignored — SIMPLNX port regression, fixed; not a deviation +**Affected users:** Users who disable an axis with valid face neighbours in DREAM3D-NX releases that contain the regression. All-directions-on examples cannot detect this defect. -Before this V&V pass, `Algorithms/ErodeDilateMask.cpp` never read `XDirOn`, `YDirOn`, or `ZDirOn`, so -the `X Direction` / `Y Direction` / `Z Direction` parameters had no effect on which face neighbours -participated. DREAM3D 6.5.171 honours all three (`ErodeDilateMask.cpp:227-247` gates each of the six -neighbour indices on the corresponding flag), so legacy was correct and SIMPLNX was wrong. Under the -bug-adjudication protocol that is a **port regression**, not a deviation from legacy behaviour: it was -fixed in the same change that found it, and once fixed no difference remains, so it gets no deviation -entry of its own. It is described in the V&V report's Summary and Bug Fixes sections. The -post-fix binary A/B (28/28 matches) is the confirmation of parity. +**Recommendation:** Trust SIMPLNX on this branch. The algorithm now honors each selected axis. The release after v7.4.2 contains the correction. -Same defect class as `ErodeDilateBadDataFilter-D1` (PR #1687); the fix mirrors that pattern and the -axis mapping was copied verbatim from `Algorithms/ErodeDilateBadData.cpp:64-80` rather than -re-derived, per that entry's branch-history note. +**Evidence:** `Class 1 dilate honours direction flags` checks X-off, `Class 1 dilate with Y direction off` checks Y-off, and `Class 1 erode honours direction flags` checks Z-off. The current one-dimensional test also checks disabled-axis dilation and erosion on X and Z. These tests are in `src/Plugins/SimplnxCore/test/ErodeDilateMaskTest.cpp`. -### `maskCopy` initial value +**Release-source evidence:** `upstream/release/v7.0.0` through `upstream/release/v7.4.0` gate all six faces in `Filters/Algorithms/ErodeDilateMask.cpp`. `upstream/release/v7.4.1` and `upstream/release/v7.4.2` do not read the direction flags in that algorithm. DREAM.3D tag `v6.5.171`, `Source/Plugins/Processing/ProcessingFilters/ErodeDilateMask.cpp:227-250`, gates all six faces. Both fixes are present in this branch and will be in the DREAM3D-NX release after v7.4.2. -Legacy allocates `_INTERNAL_USE_ONLY_MaskCopy` and calls `initializeWithValue(false)`; SIMPLNX uses a -`std::vector(totalPoints, false)`. Neither initial value can be observed: the first statement -of every iteration copies `mask` over the whole of `maskCopy` before anything reads it. Not a -deviation. +**Comparison evidence:** Run against DREAM.3D v6.5.171 on 2026-08-18: 28 of 28 mask cases match after the direction fix. See `vv_work:ErodeDilateMaskFilter/ReadMe.md` and `vv_work:ErodeDilateMaskFilter/results_compare.txt`. The direction-flag regression tests pass in CI at `01809c236`. diff --git a/src/Plugins/SimplnxCore/vv/provenance/6_6_erode_dilate_test.md b/src/Plugins/SimplnxCore/vv/provenance/6_6_erode_dilate_test.md index 66e9971716..e193143ead 100644 --- a/src/Plugins/SimplnxCore/vv/provenance/6_6_erode_dilate_test.md +++ b/src/Plugins/SimplnxCore/vv/provenance/6_6_erode_dilate_test.md @@ -1,6 +1,6 @@ # Exemplar Archive Provenance: 6_6_erode_dilate_test.tar.gz -This sidecar records the origin of the exemplar archive used by the three Erode/Dilate filter unit tests. The archive name and SHA512 must match `download_test_data()` in `src/Plugins/SimplnxCore/test/CMakeLists.txt`. +This sidecar records the shared Erode/Dilate exemplar archive and the ErodeDilateMaskFilter comparison evidence. --- @@ -11,78 +11,61 @@ This sidecar records the origin of the exemplar archive used by the three Erode/ | **Archive** | `6_6_erode_dilate_test.tar.gz` (68,857,947 bytes) | | **SHA512** | `5f0773e5d296936effbb2239965f5847e7c18533b0a2c3ec6a1d6a83b03417e5b459cce29808c8e0273613b3b6fa032c675e84926eb35d8da8a6ddc0641a0ef5` | | **Used by tests** | `SimplnxCore::ErodeDilateBadDataFilter(Erode)`; `SimplnxCore::ErodeDilateMaskFilter(Dilate)` and `(Erode)`; `SimplnxCore::ErodeDilateCoordinationNumberFilter` | -| **Generated by** | ** — the embedded pipeline references `/Users/mjackson/Applications/Data/Output/Reconstruction/SmallIN100.h5ebsd`, so it was authored by Michael Jackson | -| **Generated on** | **; archive mtime on the shared data mirror is 2026-02-19 | -| **Generated at commit** | ** — see the embedded pipeline's `FilterVersion` strings below | +| **Generated by** | ** | +| **Generated on** | ** | +| **Generated at commit** | ** | -SHA512 verified 2026-08-14 against both `test/CMakeLists.txt` and the on-disk archive in the shared `DREAM3D_Data/TestFiles` mirror. They match. +Read-only inspection on 2026-10-20 confirmed that the archive SHA512 matches `src/Plugins/SimplnxCore/test/CMakeLists.txt:234` and the value above. -## Contents +## How it was generated + +A local proof version produced the archive through pipelines embedded at `/Pipeline/Pipeline` in the `.dream3d` files. The mask pipeline imports slices 1 through 20 from `SmallIN100.h5ebsd`, reconstructs the data, and copies `Input Data` into two exemplar containers. Steps 08 and 10 run `ErodeDilateMask` with `Direction` 1 (erode) and 0 (dilate), respectively; both use two iterations with X, Y and Z enabled. Step 11 writes the mask file with `DataContainerWriter`. -| File | Consumer | +| File in `6_6_erode_dilate_test/` | Consumer | |---|---| | `6_6_erode_dilate_bad_data.dream3d` + `.xdmf` | `ErodeDilateBadDataFilter` | | `6_6_erode_dilate_mask.dream3d` + `.xdmf` | `ErodeDilateMaskFilter` | | `6_6_erode_dilate_coordination_number.dream3d` + `.xdmf` | `ErodeDilateCoordinationNumberFilter` | -Any regeneration must account for all three consumers, not just `ErodeDilateBadData`. +The embedded annotations record a local change that disables feature-ID randomization. Their statement about using one slice conflicts with the import settings and the stored 189×201×20 geometries in the mask and bad-data files. The stored mask arrays have shape `(20, 201, 189, 1)` and type `uint8`. -`6_6_erode_dilate_bad_data.dream3d` is in the legacy (`DataContainers`) layout, `FileVersion` 7.0, and holds three data containers on the same 189×201×20 `ImageGeom` (759,780 cells): - -| Container | Role | -|---|---| -| `Input Data` | Filter input. `EBSD Scan Data`: `Confidence Index` float32, `EulerAngles` float32×3, `FeatureIds` int32, `Image Quality` float32, `Mask` uint8, `Phases` int32. Plus `Grain Data/Active` uint8 (1104 tuples) and `Phase Data` (`CrystalStructures`, `LatticeConstants`, `MaterialName`). | -| `Exemplar Bad Data Erode` | Expected output for Erode, all directions on, 2 iterations. Same arrays plus `IPFColor` uint8×3. | -| `Exemplar Bad Data Dilate` | Expected output for Dilate, all directions on, 2 iterations. Same arrays plus `IPFColor` uint8×3. | +The bad-data file has an 18-step embedded pipeline. Steps 12 and 15 run `ErodeDilateBadData` with `Direction` 1 and 0, two iterations, all axes enabled and no ignored arrays. Any archive regeneration must account for all three consumers. -## How it was generated +## Canonical oracle output -The archive was produced by a legacy DREAM3D pipeline that is **embedded in the `.dream3d` file itself** (`/Pipeline/Pipeline`, `Pipeline Version` 2) — so the provenance is self-documenting even though no external `.d3dpipeline` was preserved. The 18-step pipeline reads the first slice of Small IN100 from `SmallIN100.h5ebsd`, performs a standard reconstruction, then branches twice to produce the two exemplars: +The archive supplies regression references. The independent correctness oracle for ErodeDilateMaskFilter is Class 1 and lives in `src/Plugins/SimplnxCore/test/ErodeDilateMaskTest.cpp`. -| Step | Filter | +| DataPath | Source of expected values | |---|---| -| 00 | `PipelineAnnotation` | -| 01 | `ReadH5Ebsd` → `Input Data` | -| 02–09 | `MultiThresholdObjects`, `ConvertOrientations`, `AlignSectionsMisorientation`, `IdentifySample`, `AlignSectionsFeatureCentroid`, `BadDataNeighborOrientationCheck`, `NeighborOrientationCorrelation`, `EBSDSegmentFeatures` | -| 10 | `RemoveArrays` | -| 11–13 | `CopyDataContainer` → `Exemplar Bad Data Erode`, `ErodeDilateBadData`, `GenerateIPFColors` | -| 14–16 | `CopyDataContainer` → `Exemplar Bad Data Dilate`, `ErodeDilateBadData`, `GenerateIPFColors` | -| 17 | `DataContainerWriter` | +| `Exemplar Mask Erode/EBSD Scan Data/Mask` | Local proof version, embedded mask pipeline step 08 | +| `Exemplar Mask Dilate/EBSD Scan Data/Mask` | Local proof version, embedded mask pipeline step 10 | +| `Exemplar Bad Data Erode/EBSD Scan Data/*` | Local proof version, embedded bad-data pipeline step 12 | +| `Exemplar Bad Data Dilate/EBSD Scan Data/*` | Local proof version, embedded bad-data pipeline step 15 | +| `Input Data/EBSD Scan Data/*` | Filter inputs | -The `PipelineAnnotation` step records that the pipeline was run against a build where `EBSDSegmentFeatures` had `RandomizeFeatureIds` forced to `false` internally, so that `FeatureIds` are reproducible for unit testing, and that only the first slice of Small IN100 was used with minimal processing. +Both mask exemplar tests run two iterations with all axes enabled, compare the cell arrays with the matching exemplar, and check tuple dimensions. These settings cannot detect ignored direction flags. The archive is separate from the stock DREAM.3D v6.5.171 comparison below. -The two `ErodeDilateBadData` steps are the **legacy SIMPL filter**, `Filter_Uuid` `{3adfe077-c3c9-5cd0-ad74-cf5f8ff3d254}`, with these parameters: +## Oracle provenance (Classes 2, 3, 5 only) -| Step | `Direction` | `NumIterations` | `XDirOn` / `YDirOn` / `ZDirOn` | `IgnoredDataArrayPaths` | -|---|---|---|---|---| -| 12 (Erode container) | 1 (Erode) | 2 | 1 / 1 / 1 | `[]` | -| 15 (Dilate container) | 0 (Dilate) | 2 | 1 / 1 / 1 | `[]` | +ErodeDilateMaskFilter uses Class 1, so no external correctness-oracle provenance block is required. The following artifacts document the separate legacy comparison. -These match the arguments the `(Erode)` unit test passes to the SIMPLNX filter, so the comparison is apples-to-apples. +Run against DREAM.3D v6.5.171 on 2026-08-18: 28 of 28 mask cases match. `vv_work:ErodeDilateMaskFilter/ReadMe.md` identifies the stock `PipelineRunner` and records the run date. `vv_work:ErodeDilateMaskFilter/results_compare.txt` gives the 28 element-wise matches from `vv_work:ErodeDilateMaskFilter/compare.py`. -## Canonical oracle output +The comparison uses a 5×5×1 centre seed and a 3×3×3 hollow cube. Each fixture runs dilation and erosion for one iteration on each of seven non-empty axis combinations. The Z-only cases on the single-slice fixture preserve the input; the cube supplies the Z-direction evidence. -| DataPath | Source of expected values | -|---|---| -| `Exemplar Bad Data Erode/EBSD Scan Data/*` | **Class 2** — legacy SIMPL `ErodeDilateBadData`, Erode, 2 iterations, all directions on (pipeline step 12) | -| `Exemplar Bad Data Dilate/EBSD Scan Data/*` | **Class 2** — legacy SIMPL `ErodeDilateBadData`, Dilate, 2 iterations, all directions on (pipeline step 15) | -| `Input Data/EBSD Scan Data/*` | Not an oracle — filter input | - -`ErodeDilateBadDataTest.cpp` currently consumes only the Erode exemplar. The Dilate exemplar is present in the archive and unused; wiring up a `(Dilate)` production-scale test would close that gap at no data cost. - -## Oracle provenance detail +`vv_work:ErodeDilateMaskFilter/results_oracle_check.txt` confirms that both binaries match five hand-derived masks from `vv_work:ErodeDilateMaskFilter/oracle_check.py`: -### Class 2 — Reference implementation - -- **Reference:** legacy SIMPL `ErodeDilateBadData` filter, UUID `{3adfe077-c3c9-5cd0-ad74-cf5f8ff3d254}` -- **Exact version:** the embedded pipeline records `FilterVersion` `6.6.338` for the two `ErodeDilateBadData` steps and for the surrounding EBSD reconstruction filters. This is **later than the 6.5.171 V&V baseline** — see the caveat below. -- **Runner:** legacy DREAM3D `PipelineRunner` (`DataContainerWriter` at step 17) -- **Pipeline file:** not preserved externally; embedded at `/Pipeline/Pipeline` inside `6_6_erode_dilate_bad_data.dream3d` -- **Input dataset:** first slice of Small IN100 (`SmallIN100.h5ebsd`), not synthetic +| Case | Expected true indices | +|---|---| +| 5×5×1, dilate, XYZ | `{7, 11, 12, 13, 17}` | +| 5×5×1, erode, XYZ | `{}` | +| 5×5×1, dilate, YZ | `{7, 12, 17}` | +| 3×3×3, erode, XYZ | All indices 0–26 except `{4, 10, 12, 13, 14, 16, 22}` | +| 3×3×3, erode, XY | All indices 0–26 except `{10, 12, 13, 14, 16}` | -**Baseline caveat.** The archive's `6_6_` prefix and the `6.6.338` filter version mean this exemplar comes from a legacy build *later* than the 6.5.171 baseline this V&V cycle compares against. That does not weaken it as an oracle — it is still genuine legacy output from an independent implementation, and the algorithm did not change between those legacy versions (confirmed by the line-by-line source diff recorded in the V&V report's Algorithm Relationship section). It does mean the archive alone is not a 6.5.171 comparison. The 6.5.171 comparison of record is the separate 28-combination `PipelineRunner` A/B run documented in the V&V report's Oracle section. +The zero-iteration probe in `vv_work:ErodeDilateMaskFilter/ReadMe.md` records DREAM.3D v6.5.171 rejecting the input with `-5555` while SIMPLNX returned the unchanged mask. Its inputs are `vv_work:ErodeDilateMaskFilter/extra/legacy_numiter0.json` and `vv_work:ErodeDilateMaskFilter/extra/nx_numiter0.d3dpipeline`. `ErodeDilateMaskFilter-D1` documents the correction to this validation difference. -**Direction-gating caveat.** Both exemplars were generated with all three directions enabled, so this archive cannot discriminate direction gating — a SIMPLNX build with `ErodeDilateBadDataFilter-D1` still present passes the `(Erode)` test. That is precisely the gap the 28-combination inline sweep exists to close. Do not treat this archive as sufficient coverage on its own. +The current test source is identical to `01809c236`. All registered ErodeDilateMaskFilter tests pass in CI at `01809c236` on Linux, macOS and Windows. ## Second-engineer oracle review @@ -92,6 +75,4 @@ These match the arguments the `(Erode)` unit test passes to the SIMPLNX filter, ## Regenerated to fix a circular-oracle situation? -No. This archive was **not** regenerated from SIMPLNX output at any point — the embedded pipeline proves the exemplars were written by the legacy SIMPL filter, not by SIMPLNX. The archive is retained unchanged. - -The circular-oracle concern raised during V&V applied to a different artifact: the 28 inline expected-value constants in `ErodeDilateBadDataTest.cpp`, which an earlier revision of the report described as hand-traced while they were in fact byte-identical across all 7 direction combinations (the signature of `ErodeDilateBadDataFilter-D1`). Those constants were regenerated from the 6.5.171 binary and are now a genuine Class 2 oracle. See the V&V report's Oracle section. +No. The embedded pipelines identify the local proof version as the producer of the stored exemplars. The archive is retained unchanged. The hand-derived mask expectations in the unit tests remain the independent correctness oracle. From 1b4fa7204a1c580ff7e7e5bb582af7119cde21cc Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 20 Oct 2026 19:52:17 -0400 Subject: [PATCH 10/91] DOC: Rewrite the ComputeFeatureSizes V&V record to follow the report template * Keep only the template sections and fields. * List the 16 current test cases and map 44 code paths to them. * State the release ranges of D3 to D5 from the release branches and the DREAM.3D v6.5.171 source. D3 (2D flat spacing) is still open. * Remove repeated template blocks from the provenance sidecar. Signed-off-by: Michael Jackson --- .../vv/ComputeFeatureSizesFilter.md | 193 ++++++----- .../deviations/ComputeFeatureSizesFilter.md | 111 ++++--- .../provenance/ComputeFeatureSizesFilter.md | 305 +++++++----------- 3 files changed, 305 insertions(+), 304 deletions(-) diff --git a/src/Plugins/SimplnxCore/vv/ComputeFeatureSizesFilter.md b/src/Plugins/SimplnxCore/vv/ComputeFeatureSizesFilter.md index 31663d14b9..0338f91598 100644 --- a/src/Plugins/SimplnxCore/vv/ComputeFeatureSizesFilter.md +++ b/src/Plugins/SimplnxCore/vv/ComputeFeatureSizesFilter.md @@ -1,112 +1,147 @@ -# 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. | ## 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 registered tests pass in CI at `01809c236`; the flat-spacing case is an expected failure (`ComputeFeatureSizes-D3`). | +| Code paths enumerated | 24 of 44 paths have test assertions. Gaps include input read errors, count overflow, and several cancellation paths. | +| Tests today | 16 test cases: 14 visible and 2 hidden OOC cases. Coverage includes analytical values, output writes, invalid inputs, 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 | Source comparison with DREAM.3D v6.5.171 confirms D1–D3. Runtime comparison artifacts are not available. | +| Bug flags | `ComputeFeatureSizes-D3` | +| V&V phase | Complete. `ComputeFeatureSizes-D3` remains open; its regression test is marked as an expected failure. | ## 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 registered tests pass in CI at `01809c236`. The flat-spacing case retains `[!shouldfail]` for `ComputeFeatureSizes-D3`. + +*Second-engineer review:* Michael A. Jackson, 2026-07-28 (recorded sign-off). -| 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. +PR #1638 fixed `ComputeFeatureSizes-D4` and `ComputeFeatureSizes-D5`, and DREAM3D-NX v7.4.2 contains those fixes. -*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 in this branch | +|-----------|--------|----------------------------|---------------------------| +| `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. | Subtract 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.* | +24 of 44 paths have 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` (199 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.* | ## 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. PR #1638 changed float comparisons to 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. PR #1638 changed float comparisons to 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. PR #1638 changed float comparisons to 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. PR #1638 changed float comparisons to 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. PR #1638 changed float comparisons to 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. PR #1638 changed float comparisons to 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. | ## 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 locally patched legacy 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/deviations/ComputeFeatureSizesFilter.md b/src/Plugins/SimplnxCore/vv/deviations/ComputeFeatureSizesFilter.md index aabe43c946..81d001cb91 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/ComputeFeatureSizesFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/ComputeFeatureSizesFilter.md @@ -1,80 +1,111 @@ -# Deviations from DREAM3D 6.5.171: ComputeFeatureSizes +# Deviations from DREAM.3D v6.5.171: ComputeFeatureSizesFilter -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 differences from DREAM.3D v6.5.171 (D1–D3). Part 2 lists SIMPLNX defects (D4–D5). -Entries are referenced by stable ID (`ComputeFeatureSizes-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 `ComputeFeatureSizes-D`. The Filter UUID is the permanent cross-reference anchor. Source and release evidence are in `../provenance/ComputeFeatureSizesFilter.md`. --- -## ImageGeom path — one open divergence (2D area, pending design decision) +## Part 1 — Differences from DREAM.3D v6.5.171 -Source-inspection comparison of `ProcessImageGeom` against DREAM3D 6.5.171 `FindSizes::findSizesImage()` confirmed identical algorithmic structure for 3D and for voxel counting: +### ComputeFeatureSizes-D1 -- Per-feature voxel count: same accumulation logic (SIMPLNX parallelizes it via TBB; legacy is serial — the reduction is exact integer addition either way). -- 3D volume formula: `volume = voxelCount × voxelVolume`; ESD: `2·∛(volume / (4π/3))` — both use the standard sphere formula. +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeFeatureSizes-D1` | +| **Filter UUID** | `c666ee17-ca58-4969-80d0-819986c72485` | +| **Status** | active | -> **OPEN — 2D area formula (exactly one dim == 1), pending design decision:** SIMPLNX multiplies **all three** spacings for the 2D per-voxel size (`spacing[0]·spacing[1]·spacing[2]`), following the PR #1590 "slab" convention that standardized 2D Image Geometry handling NX-wide (matching `ImageGeom::findElementSizes`). Legacy 6.5.171 uses only the **two non-flat** resolutions (a true area). The outputs therefore diverge whenever the flat dimension's spacing is not 1.0: `Volumes` scale by the flat spacing and `EquivalentDiameters` (circle-ECD from that value) by its square root. Note the current 2D branch feeds this three-spacing product into the circle-ECD formula `2·√(x/π)`, which is dimensionally inconsistent (a volume into an area formula). A legacy-matching fix was applied and then deliberately reverted (see PR #1638 discussion) because it conflicted with the #1590 convention without a team decision; the divergence is characterized by the `2D area excludes the flat-dimension spacing` test (tagged `[!shouldfail]`, flat X/Y/Z orientations, flat spacing 5.0), which asserts the legacy value and fails-as-expected until the convention is settled. Resolution options: (A) adopt legacy true-2D semantics here and in `findElementSizes`, or (B) keep the slab convention, switch the 2D branch to ESD for dimensional coherence, and promote this entry to a numbered deviation. +**Symptom:** RectGrid feature volumes can differ from DREAM.3D v6.5.171 because the sums use different precision. -**Precision notes (verified against 6.5.171 source, not flagged as deviations):** +**Root cause:** Precision. DREAM.3D v6.5.171 `FindSizes::findSizesUnstructured()` adds float32 cell sizes to a float32 feature volume. SIMPLNX promotes each cell size to float64 and uses Kahan compensated summation. Direct execution uses thread-local sums and a compensated reduction. Its per-worker compensation resets at each `RectGridSummationImpl::convert()` call. Scanline execution keeps one compensation value per feature across input chunks. These mechanisms reduce accumulation error, but do not guarantee a strictly more accurate result for every input. -- *3D `Volumes`: bit-identical.* Both implementations compute the voxel volume as a float32 product of the three float32 spacings (SIMPLNX widens the float32 result to float64 afterward; legacy casts it to double) and multiply by the voxel count in double before storing as float32 — the same roundings in the same order. -- *2D `Areas`: potential ≤1 float32 ULP difference even when the flat spacing is 1.0.* Setting aside the open three-vs-two-spacing divergence above (which dominates whenever the flat spacing ≠ 1.0), the evaluation precision also differs: SIMPLNX computes the spacing product in float64; legacy `findSizesImage` rounds the two-resolution product to float32 first (`res_scalar = xRes * yRes`). When that product is inexact in float32, the stored per-feature area can differ by one ULP. Not observable in the current fixtures and non-material for downstream statistics; no A/B run has been performed for the ImageGeom path. -- *ESD/ECD evaluation:* legacy uses float32 `powf`/`sqrtf` on the float32-rounded stored value, SIMPLNX uses float64 `std::cbrt`/`std::sqrt` on the unrounded float64 value — this is the same evaluation-precision pattern documented (and A/B-verified on RectGrid) as `ComputeFeatureSizes-D2`. +**Affected users:** Users who compare RectGrid feature volumes or derived diameters with DREAM.3D v6.5.171 output. + +**Recommendation:** Trust the analytical volume definition and compare results with an appropriate numerical tolerance. SIMPLNX retains more precision during accumulation; `ComputeFeatureSizes-D2` also affects the diameter. + +**Evidence:** Source comparison uses `Source/Plugins/Statistics/StatisticsFilters/FindSizes.cpp` at the DREAM.3D `v6.5.171` tag and the current `ComputeFeatureSizesDirect.cpp` and `ComputeFeatureSizesScanline.cpp`. --- -## ComputeFeatureSizes-D1 +### ComputeFeatureSizes-D2 | Field | Value | |---|---| -| **Deviation ID** | `ComputeFeatureSizes-D1` | +| **Deviation ID** | `ComputeFeatureSizes-D2` | | **Filter UUID** | `c666ee17-ca58-4969-80d0-819986c72485` | -| **Affected array** | `Volumes` (RectGridGeom) | -| **Status** | Active — precision improvement; SIMPLNX output is more accurate. **Verified by A/B (2026-06-27)** — see "A/B verification" below. | +| **Status** | active | + +**Symptom:** Equivalent diameters can differ from DREAM.3D v6.5.171 even when the stored feature sizes agree. -**Symptom:** Per-feature volumes for features in `RectGridGeom` geometries differ from DREAM3D 6.5.171 `FindSizes::findSizesUnstructured()` output. The discrepancy grows with feature size and with the variation in cell volumes across the grid, scaling approximately as `O(N · ε_float32)` for the legacy result vs `O(ε_float64)` for the SIMPLNX result, where N is the number of cells in the feature. +**Root cause:** Precision and library. DREAM.3D v6.5.171 reads the stored float32 size into the diameter calculation and uses `powf` or `sqrtf`. SIMPLNX uses the unrounded float64 feature size with `std::cbrt` or `std::sqrt`. For RectGrid, the denominator also changes from float32 `(4/3)*pi` to float64 `(4*pi)/3`. The same stored-size versus unrounded-size distinction applies to Image geometry. -**Root cause:** Precision — two compounding improvements in SIMPLNX's `ProcessRectGridGeom` over the legacy `findSizesUnstructured`: +**Affected users:** Users who compare equivalent spherical or circular diameters with DREAM.3D v6.5.171 output. -1. **float32 → float64 promotion.** Element sizes are stored as `float32` (the output of `findElementSizes`). The legacy `findSizesUnstructured` accumulated these directly in `float32`, so each per-cell addition carried a relative error of ~`ε_float32 ≈ 1.2×10⁻⁷`. SIMPLNX promotes each element size to `float64` before accumulation (`static_cast(m_ElemSizes.getValue(voxelIdx))`), reducing the per-operation rounding error to `ε_float64 ≈ 2.2×10⁻¹⁶`. +**Recommendation:** Trust the analytical diameter formula. Use a numerical tolerance when comparing implementations with different intermediate precision. -2. **Kahan summation.** SIMPLNX applies Kahan compensated summation at two levels: - - *Per-thread, per-cell* in `RectGridSummationImpl::convert()` (lines 293–305): standard Kahan with `y = elemSize - c`, `t = sum + y`, `c′ = (t - sum) - y`. For a feature spanning N cells, naive float64 accumulation still has O(N · ε_float64) error; Kahan reduces this to O(ε_float64) by carrying a compensation term `c` that recovers the low-order bits lost in each `sum + y` operation. (Note the compensator is a local that resets on each TBB body invocation — see the V&V report's D1 summary.) - - *Post-reduction, per thread-local vector* in `threadLocalVolumes.combine_each()` (lines 368–385): the same Kahan scheme is applied when combining the TBB thread-local partial sums into the final per-feature volume. +**Evidence:** The DREAM.3D `v6.5.171` source and the current Direct and Scanline sources use the evaluation order and types described above. + +--- - For a non-uniform rectilinear grid where cell volumes span several orders of magnitude (e.g., a grid with fine near-surface resolution and coarse interior), naive summation of N cells can lose ~`log₂(V_max / V_min)` bits of precision in the smaller cell contributions. Kahan summation recovers those bits regardless of N or the dynamic range of cell volumes. +### ComputeFeatureSizes-D3 -**Affected users:** Any workflow that runs `ComputeFeatureSizes` on a `RectGridGeom` and then compares per-feature volume or ESD values against DREAM3D 6.5.171 output. The deviation is largest for large features (high N) on grids with high volume variation. On uniform RectGrids (all cell volumes equal), Kahan has no practical effect and the only deviation is the float32→float64 promotion. +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeFeatureSizes-D3` | +| **Filter UUID** | `c666ee17-ca58-4969-80d0-819986c72485` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.4.2. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | Not fixed. | -**Recommendation:** Trust SIMPLNX. The legacy `findSizesUnstructured` accumulated unnecessary floating-point error that grew with feature size and grid non-uniformity. The SIMPLNX result is strictly more accurate. Users migrating pipelines should expect small positive or negative shifts in per-feature volumes. The `EquivalentDiameters` array is affected both by this volume shift **and** by a separate ESD-evaluation deviation — see `ComputeFeatureSizes-D2`. +**Symptom:** For a 2D image, the reported area includes the flat dimension's spacing, and the circular diameter uses that value. -**A/B verification (2026-06-27):** A direct comparison was run, not just source inspection. The exact RectGrid unit-test fixture was authored as a shared legacy `.dream3d` and run through stock DREAM3D 6.5.171, DREAM3D-NX, and a locally patched legacy build. Stock 6.5.171 `Volumes` differed from SIMPLNX (≈1 float32 ULP on this small fixture; grows with N). Applying **only** the float64 + Kahan summation change to legacy `findSizesUnstructured` made `Volumes` **bit-identical** to SIMPLNX, confirming summation precision as the sole root cause of the `Volumes` deviation. +**Root cause:** Bug in the area and diameter interpretation. PR #1590 (`e76ad508e`) replaced the two non-flat spacings with the product of all three spacings. Both current algorithms keep this slab convention and then apply `2*sqrt(value/pi)`. With physical length units, the product is a volume, so it is not an area for the circle formula. DREAM.3D v6.5.171 uses only the two non-flat spacings. + +**Affected users:** Users who need true 2D area and circular diameter, especially when flat spacing is not 1. For four cells with non-flat spacings 2 and 3 and flat spacing 5, the algorithms calculate 120; the true area is 24. Relative to true area, the size scales by flat spacing and the diameter scales by its square root. + +**Recommendation:** Do not interpret these outputs as true area and circular diameter when flat spacing is not 1. For true 2D measurements, use the product of the two non-flat spacings for area and `2*sqrt(area/pi)` for diameter. + +**Evidence:** `SimplnxCore::ComputeFeatureSizes: 2D area excludes the flat-dimension spacing` asserts 24 for flat X, Y, and Z and has `[!shouldfail]`. The Scanline `bounded output writes` Image 2D variant instead asserts the current slab result. Source in `upstream/release/v7.0.0` through `upstream/release/v7.4.1` excludes the flat spacing. Source in `upstream/release/v7.4.2` includes all three spacings. --- -## ComputeFeatureSizes-D2 +## Part 2 — SIMPLNX defects and behavior changes + +### ComputeFeatureSizes-D4 | Field | Value | |---|---| -| **Deviation ID** | `ComputeFeatureSizes-D2` | +| **Deviation ID** | `ComputeFeatureSizes-D4` | | **Filter UUID** | `c666ee17-ca58-4969-80d0-819986c72485` | -| **Affected array** | `EquivalentDiameters` (RectGridGeom; same evaluation pattern on ImageGeom) | -| **Status** | Active — precision improvement; SIMPLNX output is more accurate. **Verified by A/B (2026-06-27).** | +| **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** | PR #1638 (`8d0b0ed22`), DREAM3D-NX v7.4.2. | -**Symptom:** Per-feature `EquivalentDiameters` for `RectGridGeom` geometries differ from DREAM3D 6.5.171 output by ≈1 float32 ULP **even after the `Volumes` arrays are made identical**. This is a second precision source, independent of the summation in D1. +**Symptom:** RectGrid feature-volume reduction could lose the intended compensation for floating-point rounding. -**Root cause:** The equivalent spherical diameter evaluation differs: -- **Legacy `findSizesUnstructured`:** `rad = m_Volumes[i] / vol_term; diameter = 2.0f * powf(rad, 0.3333333333f)` — the cube root is taken with `powf` (float32) on the **float32-rounded** stored volume, and `vol_term` is `(4/3)·k_Pif` (float32 π). -- **SIMPLNX `ProcessRectGridGeom`:** `2.0 * std::cbrt(featureVolumes[i] / k_ESDVolumeDenominator)` — `std::cbrt` (float64) on the **float64** accumulated volume, with `k_ESDVolumeDenominator = (4·π)/3` in float64. +**Root cause:** Bug. PR #1592 (`aaad2c4e2`) added the parallel reduction. It subtracted the compensation from the accumulated total, added that value to the incoming partial sum, and measured the rounding residual relative to the incoming sum. PR #1638 applies the usual Kahan update: subtract the compensation from the incoming partial sum, add it to the total, and calculate the residual relative to the total. The corrected code is in `ComputeFeatureSizesDirect.cpp`. -So even with identical `Volumes`, the ESD differs because the legacy path rounds the volume to float32 first and uses a float32 `powf` cube root, while SIMPLNX evaluates the cube root in float64 on the unrounded sum. +**Affected users:** Users of development revisions between PR #1592 and PR #1638 who calculate RectGrid feature volumes. DREAM3D-NX v7.0.0 through v7.4.2 were not affected. -**A/B verification (2026-06-27):** After making `Volumes` identical (D1 patch), `EquivalentDiameters` still differed by ≈1 ULP. Additionally changing legacy to compute the diameter as `2.0 * std::cbrt(volumeSums[i] / ((4.0·k_Pi)/3.0))` (double `std::cbrt` on the double sum) made `EquivalentDiameters` **bit-identical** to SIMPLNX. Both changes together — D1 (summation) and D2 (ESD evaluation) — are required for legacy to fully reproduce SIMPLNX output. +**Recommendation:** Use a revision that contains PR #1638. The RectGrid analytical tests check feature volumes and diameters, but do not isolate the compensation correction with an adversarial rounding fixture. -**Affected users:** Any workflow comparing `EquivalentDiameters` against DREAM3D 6.5.171. The same `powf`-on-float32 vs `cbrt`-on-float64 pattern exists on the ImageGeom path (`FindSizes::findSizesImage` uses `powf`/`sqrtf`); it was previously noted only as a non-flagged precision difference and is folded into this entry for the equivalent-diameter/ECD evaluation. +--- -**Recommendation:** Trust SIMPLNX. The float64 `std::cbrt`/`std::sqrt` evaluation is more accurate than the legacy float32 `powf`/`sqrtf` on a pre-rounded value. +### ComputeFeatureSizes-D5 ---- +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeFeatureSizes-D5` | +| **Filter UUID** | `c666ee17-ca58-4969-80d0-819986c72485` | +| **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** | PR #1638 (`8d0b0ed22`), DREAM3D-NX v7.4.2. | + +**Symptom:** Parallel Image and RectGrid workers could read nonresident FeatureIds without first disabling parallel scheduling. + +**Root cause:** Bug. PR #1592 introduced parallel reads without a residency guard. PR #1638 added `requireStoresInMemory()` for FeatureIds before each `ParallelDataAlgorithm::execute()` call. The current Direct implementation retains both calls. The guard controls scheduling; it does not pin data or synchronize store access. The RectGrid guard checks FeatureIds only, not element sizes. + +**Affected users:** Users of development revisions between PR #1592 and PR #1638 with nonresident FeatureIds and parallel execution. DREAM3D-NX v7.0.0 through v7.4.2 were not affected. -*If a future comparison run against DREAM3D 6.5.171 output reveals additional deviations, add them here as `ComputeFeatureSizes-D3` etc.* +**Recommendation:** Use a revision that contains PR #1638. Current real-store tests require Scanline dispatch and do not directly exercise this Direct scheduling fallback. diff --git a/src/Plugins/SimplnxCore/vv/provenance/ComputeFeatureSizesFilter.md b/src/Plugins/SimplnxCore/vv/provenance/ComputeFeatureSizesFilter.md index e6daec9ece..31d223768c 100644 --- a/src/Plugins/SimplnxCore/vv/provenance/ComputeFeatureSizesFilter.md +++ b/src/Plugins/SimplnxCore/vv/provenance/ComputeFeatureSizesFilter.md @@ -1,25 +1,37 @@ -# Exemplar Archive Provenance: Inlined +# Exemplar Archive Provenance: 6_6_stats_test_v2.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?" +This sidecar records the regression archive and the separate Class 1 fixtures used by `ComputeFeatureSizesFilter`. -One sidecar per archive. The archive name and SHA512 must match `download_test_data()` in `src/Plugins/

/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` | ---- +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. + +**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. Relevant Information for the data set: @@ -52,15 +64,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. + +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 +``` + +**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. + +Relevant Information for the data set: + +```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 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 retains `[!shouldfail]`: it expects area 24 for flat X, Y, and Z, while the filter calculates 120 (`ComputeFeatureSizes-D3`). -The expected outputs are as follows: +**RectGrid derivation** ```text NumElements: {0, 39, 15, 10} @@ -68,7 +148,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 +305,9 @@ Data ======================== ``` -```text -Calculated Output - - 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 +Calculate the equivalent spherical diameter from the feature volume `V`: `D = 2*cbrt(3*V/(4*pi))`. -- **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 +315,47 @@ 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. 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): ** +- **Reviewer:** Michael A. Jackson. +- **Date:** 2026-07-28, as recorded in the report sign-off. ## 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. From db3bc97f93cdeab6975019e15992d0aef3ab1a03 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 20 Oct 2026 19:52:17 -0400 Subject: [PATCH 11/91] DOC: Rewrite the BadDataNeighborOrientationCheck V&V record to follow the template * Keep only the template sections and fields. Fold the sub-sections into the sidecars. * Add stable IDs D3 to D5 for the tolerance, Laue-index and phase-index fixes, and state the release ranges from the release branches and the DREAM.3D v6.5.171 source. * Record the DREAM.3D v6.5.171 comparison (2026-09-17): 12 of 28 cases match; a local proof version matches all 28. Signed-off-by: Michael Jackson --- .../BadDataNeighborOrientationCheckFilter.md | 255 ++++++++---------- .../BadDataNeighborOrientationCheckFilter.md | 131 ++++----- .../BadDataNeighborOrientationCheckFilter.md | 100 +++---- 3 files changed, 221 insertions(+), 265 deletions(-) diff --git a/src/Plugins/OrientationAnalysis/vv/BadDataNeighborOrientationCheckFilter.md b/src/Plugins/OrientationAnalysis/vv/BadDataNeighborOrientationCheckFilter.md index f2f030cce0..09875ce533 100644 --- a/src/Plugins/OrientationAnalysis/vv/BadDataNeighborOrientationCheckFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/BadDataNeighborOrientationCheckFilter.md @@ -1,176 +1,149 @@ # 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; 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`. All 33 ordinary test cases pass in CI at `01809c236` on Linux, macOS and Windows. | +| Code paths enumerated | 17 of 27 exercised. Defensive guards, cancellation and I/O failures have no direct fixtures. | +| Tests today | 33 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 branch contains the five documented defect fixes below. ## 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:* PR #1499 (`ebdce592f`) corrected the port; PR #1568 (`4ec21e93f`) replaced its repeated full-volume scans with Worklist and Scanline implementations. ## 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. All 33 ordinary test cases pass in CI at `01809c236`. -- 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. +This branch fixes all defects in this table. PR #1499 fixed `BadDataNeighborOrientationCheckFilter-D1` and `BadDataNeighborOrientationCheckFilter-D2`, and DREAM3D-NX v7.4.1 contains those fixes. PR #1631 fixed `BadDataNeighborOrientationCheckFilter-D3` and `BadDataNeighborOrientationCheckFilter-D4`, and DREAM3D-NX v7.4.2 contains those fixes. PR #1568 fixed `BadDataNeighborOrientationCheckFilter-D5`; that fix will be in the DREAM3D-NX release after v7.4.2. -## 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 in this branch | +|-----------|--------|----------------------------|---------------------------| +| `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. | Convert 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. | Reject invalid Laue indices with `-54901`; skip 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. | Skip nonpositive phases and return `-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.* | +17 of 27 paths exercised. The table maps source paths to existing fixtures. Each uncovered path states why it has no direct fixture. + +Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/BadDataNeighborOrientationCheck.cpp` (37 lines), `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/BadDataNeighborOrientationCheckWorklist.cpp` (256 lines), `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/BadDataNeighborOrientationCheckScanline.cpp` (382 lines), and `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/BadDataNeighborOrientationCheckFilter.cpp` (173 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. Valid positive thresholds prevent rejected voxels from entering the queue; no nonpositive-threshold fixture exists.* | +| 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. No fixture uses a neighbor requirement above six.* | ## 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: 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: 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/deviations/BadDataNeighborOrientationCheckFilter.md b/src/Plugins/OrientationAnalysis/vv/deviations/BadDataNeighborOrientationCheckFilter.md index b14f7fe2bb..679eb590b2 100644 --- a/src/Plugins/OrientationAnalysis/vv/deviations/BadDataNeighborOrientationCheckFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/deviations/BadDataNeighborOrientationCheckFilter.md @@ -1,115 +1,122 @@ -# 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 | |---|---| | **Deviation ID** | `BadDataNeighborOrientationCheckFilter-D1` | | **Filter UUID** | `3f342977-aea1-49e1-a9c2-f73760eba0d3` | | **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.0. DREAM.3D v6.5.171 was affected. | +| **Fixed in** | PR #1499, DREAM3D-NX v7.4.1. | -**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. +**Symptom:** A bad voxel can stay bad when its matching good-neighbor count equals the requested minimum. -**Root cause:** Bug in DREAM3D 6.5.171. +**Root cause:** Bug. The DREAM.3D v6.5.171 source uses `while(currentLevel > m_NumberOfNeighbors)`. It omits the minimum level, including all propagation when the requested count is six. The same condition occurs in released SIMPLNX source. PR #1499 (`ebdce592f`) changes `>` to `>=`. Both current implementations retain the inclusive bound. See `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/BadDataNeighborOrientationCheckWorklist.cpp` and `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/BadDataNeighborOrientationCheckScanline.cpp`. -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. +**Affected users:** Users whose bad voxels need propagation at the minimum neighbor level. A changed mask can change downstream results. -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:** 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. 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. +**Recommendation:** Trust SIMPLNX. The inclusive bound agrees with the parameter definition of a minimum number of neighbors. `Case 1.1.1` through `Case 1.6.1` check the minimum-level result. --- -## 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) | - -**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. - -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. +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.0. DREAM.3D v6.5.171 was affected. | +| **Fixed in** | PR #1499, DREAM3D-NX v7.4.1. | -**Root cause:** Bug in DREAM3D 6.5.171. +**Symptom:** A different-phase neighbor can inherit a small angle from an earlier neighbor and incorrectly increase the matching-neighbor count. -The legacy per-neighbor loop body is (`BadDataNeighborOrientationCheck.cpp:283-291`): +**Root cause:** Bug. The DREAM.3D v6.5.171 source places the angle test outside the same-phase condition: ```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! +if(w < misorientationTolerance) { neighborCount[i]++; } ``` -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 release snapshots also contain this structure. PR #1499 (`ebdce592f`) puts the angle test inside the same-phase condition in both counting and propagation. Worklist retains that condition; Scanline uses `isMisorientationMatch()` to reject a phase mismatch before calculating the angle. The current rewrite changes the execution method but preserves this corrected rule and the D1 inclusive bound. The comparison isolates 37 extra good voxels caused by stale angles in a local proof version. Moving both angle tests inside the phase condition removes those differences. See `vv_work:BadDataNeighborOrientationCheckFilter/algorithm_review/source_comparison.md` and `vv_work:BadDataNeighborOrientationCheckFilter/comparison/legacy_emulator_results.md`. -SIMPLNX moves both the misorientation computation AND the increment inside the same-phase conditional (`Algorithms/BadDataNeighborOrientationCheck.cpp:105-117`): +**Affected users:** Users with mixed-phase neighbors. A stale angle can overcount matches at a phase boundary. -```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]++; - } -} -``` +**Recommendation:** Trust SIMPLNX. Only a good neighbor of the same positive phase can count as a match. `Case 1.2.2` checks a mixed-phase expected mask. + +--- + +## Part 2 — SIMPLNX defects and behavior changes + +### 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** | PR #1631, DREAM3D-NX v7.4.2. | + +**Symptom:** A rounded-up angle tolerance can admit a neighbor that should fail the strict boundary comparison. -**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. +**Root cause:** Bug. The earlier algorithm calculated radians with `float` and `numbers::pi_v`. PR #1631 (`ec22ac375`) replaces both with double precision. Worklist and Scanline retain that conversion. The current expected masks for `Case 1.3.3`, `Case 1.4.3`, `Case 1.5.3` and `Case 1.6.3` keep the center bad. DREAM.3D v6.5.171 also converts the tolerance with float arithmetic. The local proof comparison isolates four center-voxel differences in these cases and removes them with a corrected tolerance conversion. The stored float32 quaternions give an angle slightly above 5°; both tolerance conversion and angle rounding affect the decision. See `vv_work:BadDataNeighborOrientationCheckFilter/algorithm_review/source_comparison.md`. -**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. +**Affected users:** Users with misorientations sufficiently close to the strict tolerance boundary. The output effect depends on the orientation calculation as well as the tolerance conversion. -**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. +**Recommendation:** Trust SIMPLNX. Double precision reduces the conversion error without changing the strict `<` comparison. --- -### EbsdLib 2.4.1 CubicOps precision improvement (precision improvement; not a behavioral deviation in this filter's test data) +### BadDataNeighborOrientationCheckFilter-D4 + +| 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** | PR #1631, 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. PR #1631 (`ec22ac375`) adds a Laue-index check and a per-voxel guard. Both current implementations return `-54901` for an invalid non-sentinel value and skip a positive phase that resolves to `UnknownCrystalStructure`. No current fixture directly tests these rejection and skip paths. -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). +**Affected users:** Users with invalid values in Crystal Structures, or a participating positive phase mapped to the unknown-structure sentinel. -**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. +**Recommendation:** Trust SIMPLNX. Supply valid Laue indices; the unknown-structure sentinel cannot take part in an orientation match. --- -## 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** | PR #1568, 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. -Verification fixtures + comparison results are at `/Users/mjackson/Workspace9/DREAM3D_Data/TestFiles/bad_data_neighbor_orientation_check_v2/`: +**Root cause:** Bug. The DREAM3D-NX v7.4.2 release snapshot reads `crystalStructures[cellPhases[voxelIndex]]` before checking the phase range. Earlier release branches and DREAM.3D v6.5.171 also read through an unchecked phase index. PR #1568 (`4ec21e93f`) checks the phase before the lookup in Worklist and Scanline. Nonpositive phases do not participate; a positive phase beyond the array returns `-54902`. `Phase Index Bounds` checks the bad-voxel error and permits an invalid phase on an already good 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. +**Affected users:** Users with phase values that do not identify an entry in Crystal Structures on a bad 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. +**Recommendation:** Trust SIMPLNX. Correct the phase or supply the corresponding ensemble entry before processing that voxel. diff --git a/src/Plugins/OrientationAnalysis/vv/provenance/BadDataNeighborOrientationCheckFilter.md b/src/Plugins/OrientationAnalysis/vv/provenance/BadDataNeighborOrientationCheckFilter.md index 1e82528caf..9f3cf2cc9d 100644 --- a/src/Plugins/OrientationAnalysis/vv/provenance/BadDataNeighborOrientationCheckFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/provenance/BadDataNeighborOrientationCheckFilter.md @@ -1,89 +1,65 @@ -# 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 — initial fixtures and test suite in PR #1499 (`ebdce592f`). | +| **Archive registration** | PR #1631 (`ec22ac375`) registers this name and SHA512 in `src/Plugins/OrientationAnalysis/test/CMakeLists.txt:173`. | -## How it was generated +The test source is identical to `01809c236`. All 33 ordinary test cases pass in CI at `01809c236` on Linux, macOS and Windows, as supplied in the pass-2 brief. The 2D, phase-bounds, tuple-count and HDF5 fixtures construct their inputs in C++. -The archive contains a hierarchy of **input-only** `.dream3d` files, one per test case, organized by parameter combination: - -``` -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)* +The report sign-off names Nathan Young (2026-02-02) and Michael Jackson (2026-06-01). Its Oracle text says that Michael Jackson reviewed the design. ## 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. From 748fadb54d33e9dfc89621c099fed86b77b96e76 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 16:16:14 -0400 Subject: [PATCH 12/91] DOC: Set V&V rules for sign-off, legacy re-runs, and inferred provenance - Sign-off uses ", YYYY-MM-DD; second engineer: , YYYY-MM-DD". The second engineer is the reviewer who approved the PR with the V&V work. - A Legacy comparison must run on a current nxrunner build. Its artifacts go to vv_work:/ on OneDrive. - When no record shows who generated an archive, use the PR author and the dates the PR was open, marked as inferred. Signed-off-by: Michael Jackson --- docs/vv_templates/provenance_template.md | 2 ++ docs/vv_templates/report_gates.md | 5 ++++- docs/vv_templates/report_template.md | 5 ++++- 3 files changed, 10 insertions(+), 2 deletions(-) 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 874d174bfa..c75f22ee13 100644 --- a/docs/vv_templates/report_gates.md +++ b/docs/vv_templates/report_gates.md @@ -12,7 +12,8 @@ A section of `src/Plugins/

/vv/.md` is "done" when all its gates p - [ ] 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 @@ -26,6 +27,8 @@ The dashboard a reviewer reads first. Lets a reviewer decide in 30 seconds wheth - [ ] **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 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 diff --git a/docs/vv_templates/report_template.md b/docs/vv_templates/report_template.md index 51dfe80c3e..fd24e60d44 100644 --- a/docs/vv_templates/report_template.md +++ b/docs/vv_templates/report_template.md @@ -13,6 +13,7 @@ - Test inventory status values other than `kept`, `new-for-V&V`, and `retired`. Put the reason for a change in Notes. - Emoji, check marks, or strike-through text. - Any legacy version other than DREAM.3D v6.5.171. A comparison on a non-stock legacy build is "a local proof version". + - A Legacy comparison result from a run on SIMPLNX code older than the current algorithm. Run the comparison again with a current `nxrunner` build when the algorithm changes. - The name "DREAM3D" for the legacy application. Write "DREAM.3D v6.5.171", or "DREAM.3D" for short. "DREAM3D-NX" is the new application. --> @@ -24,7 +25,9 @@ | DREAM.3D v6.5.171 equivalent | *or* None (new filter) | | Verified commit | ** | | Status | DRAFT *or* READY FOR REVIEW *or* COMPLETE | -| Sign-off | ** | +| Sign-off | *, YYYY-MM-DD; second engineer: , YYYY-MM-DD* | + + ## At a glance From ffa277501895c6a1cd62fbdf14c2145506dbc488 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 16:16:14 -0400 Subject: [PATCH 13/91] DOC: Apply the sign-off and provenance rules to the pilot V&V records - Use one sign-off form in DBSCAN, ErodeDilateMask, ComputeFeatureSizes, and BadDataNeighborOrientationCheck. - ErodeDilateMask: Nathan Young approved PR #1717, so the record is COMPLETE. Remove stale "this branch" text and an outdated review note. - Fill in the inferred archive provenance for all three pilot archives. Signed-off-by: Michael Jackson --- .../vv/BadDataNeighborOrientationCheckFilter.md | 2 +- .../BadDataNeighborOrientationCheckFilter.md | 9 ++++++--- .../SimplnxCore/vv/ComputeFeatureSizesFilter.md | 4 ++-- src/Plugins/SimplnxCore/vv/DBSCANFilter.md | 2 +- .../SimplnxCore/vv/ErodeDilateMaskFilter.md | 14 +++++++------- .../vv/deviations/ErodeDilateMaskFilter.md | 8 ++++---- .../vv/provenance/6_6_erode_dilate_test.md | 11 +++++------ .../vv/provenance/ComputeFeatureSizesFilter.md | 5 ++++- 8 files changed, 30 insertions(+), 25 deletions(-) diff --git a/src/Plugins/OrientationAnalysis/vv/BadDataNeighborOrientationCheckFilter.md b/src/Plugins/OrientationAnalysis/vv/BadDataNeighborOrientationCheckFilter.md index 09875ce533..e13b3bcf13 100644 --- a/src/Plugins/OrientationAnalysis/vv/BadDataNeighborOrientationCheckFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/BadDataNeighborOrientationCheckFilter.md @@ -8,7 +8,7 @@ | DREAM.3D v6.5.171 equivalent | `BadDataNeighborOrientationCheck` | | Verified commit | ** | | Status | COMPLETE | -| Sign-off | *Nathan Young, 2026-02-02; Michael Jackson, 2026-06-01* | +| Sign-off | Nathan Young, 2026-02-02; second engineer: Michael Jackson, 2026-06-01 | ## At a glance diff --git a/src/Plugins/OrientationAnalysis/vv/provenance/BadDataNeighborOrientationCheckFilter.md b/src/Plugins/OrientationAnalysis/vv/provenance/BadDataNeighborOrientationCheckFilter.md index 9f3cf2cc9d..546878cc53 100644 --- a/src/Plugins/OrientationAnalysis/vv/provenance/BadDataNeighborOrientationCheckFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/provenance/BadDataNeighborOrientationCheckFilter.md @@ -9,10 +9,13 @@ This sidecar records the test-input archive, independent expected masks, release | **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 — initial fixtures and test suite in PR #1499 (`ebdce592f`). | -| **Archive registration** | PR #1631 (`ec22ac375`) registers this name and SHA512 in `src/Plugins/OrientationAnalysis/test/CMakeLists.txt:173`. | +| **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) | -The test source is identical to `01809c236`. All 33 ordinary test cases pass in CI at `01809c236` on Linux, macOS and Windows, as supplied in the pass-2 brief. The 2D, phase-bounds, tuple-count and HDF5 fixtures construct their inputs in C++. +PR #1631 (`ec22ac375`) registers this archive name and SHA512 in `src/Plugins/OrientationAnalysis/test/CMakeLists.txt:173`. + +The test source is identical to `01809c236`. All 33 ordinary test cases pass in CI at `01809c236` on Linux, macOS and Windows. The 2D, phase-bounds, tuple-count and HDF5 fixtures construct their inputs in C++. ## How it was generated diff --git a/src/Plugins/SimplnxCore/vv/ComputeFeatureSizesFilter.md b/src/Plugins/SimplnxCore/vv/ComputeFeatureSizesFilter.md index 0338f91598..f7c9044006 100644 --- a/src/Plugins/SimplnxCore/vv/ComputeFeatureSizesFilter.md +++ b/src/Plugins/SimplnxCore/vv/ComputeFeatureSizesFilter.md @@ -8,7 +8,7 @@ | 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 @@ -41,7 +41,7 @@ *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 registered tests pass in CI at `01809c236`. The flat-spacing case retains `[!shouldfail]` for `ComputeFeatureSizes-D3`. -*Second-engineer review:* Michael A. Jackson, 2026-07-28 (recorded sign-off). +*Second-engineer review:* Michael Jackson, 2026-07-28 (recorded sign-off). ## Bugs found and fixed diff --git a/src/Plugins/SimplnxCore/vv/DBSCANFilter.md b/src/Plugins/SimplnxCore/vv/DBSCANFilter.md index 121a708b23..c0ec59c58f 100644 --- a/src/Plugins/SimplnxCore/vv/DBSCANFilter.md +++ b/src/Plugins/SimplnxCore/vv/DBSCANFilter.md @@ -10,7 +10,7 @@ | 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* | +| Sign-off | Nathan Young, 2026-08-07; second engineer: Michael Jackson, 2026-08-28 | ## At a glance diff --git a/src/Plugins/SimplnxCore/vv/ErodeDilateMaskFilter.md b/src/Plugins/SimplnxCore/vv/ErodeDilateMaskFilter.md index 6e3aa936cc..e7b5c59825 100644 --- a/src/Plugins/SimplnxCore/vv/ErodeDilateMaskFilter.md +++ b/src/Plugins/SimplnxCore/vv/ErodeDilateMaskFilter.md @@ -9,8 +9,8 @@ | SIMPLNX Human Name | Erode/Dilate Mask | | DREAM.3D v6.5.171 equivalent | `ErodeDilateMask` (Processing plugin, SIMPL UUID `4fff1aa6-4f62-56c4-8ee9-8e28ec2fcbba`) | | Verified commit | ** | -| Status | READY FOR REVIEW | -| Sign-off | *delegated to the PR reviewer (requester decision 2026-08-19)* | +| Status | COMPLETE | +| Sign-off | Michael Jackson, 2026-08-20; second engineer: Nathan Young, 2026-08-25 | ## At a glance @@ -23,11 +23,11 @@ | Exemplar archive | `6_6_erode_dilate_test.tar.gz` supplies inputs and expected outputs for the two exemplar comparisons. It does not establish parity with DREAM.3D v6.5.171. | | Legacy comparison | Run against DREAM.3D v6.5.171 on 2026-08-18: 28 of 28 mask cases match on a 5×5×1 centre seed and a 3×3×3 hollow cube. | | Bug flags | None. | -| V&V phase | Analytical fixtures and regressions for both fixed defects are encoded. All registered tests pass in CI at `01809c236`. PR-reviewer sign-off remains outstanding. | +| V&V phase | Analytical fixtures and regressions for both fixed defects are encoded. All registered tests pass in CI at `01809c236`. | ## Summary -`ErodeDilateMaskFilter` grows or shrinks a boolean cell mask by one face-neighbour layer per iteration on the selected axes. Class 1 fixtures check exact masks, and two archive tests compare reconstruction data with stored exemplars. The branch fixes ignored direction flags and acceptance of non-positive iteration counts. All registered tests pass in CI at `01809c236`, and all 28 legacy comparison cases match. +`ErodeDilateMaskFilter` grows or shrinks a boolean cell mask by one face-neighbour layer per iteration on the selected axes. Class 1 fixtures check exact masks, and two archive tests compare reconstruction data with stored exemplars. SIMPLNX fixes ignored direction flags and acceptance of non-positive iteration counts. All registered tests pass in CI at `01809c236`, and all 28 legacy comparison cases match. ## Algorithm Relationship @@ -43,13 +43,13 @@ *Encoded:* `src/Plugins/SimplnxCore/test/ErodeDilateMaskTest.cpp` — seven `Class 1` cases use three fixture layouts; `SimplnxCore::ErodeDilateMaskFilter: Literal One-Dimensional Morphology` adds two axis layouts and ten sections with exact mask and unchanged-sibling checks. All pass in CI at `01809c236`. -*Second-engineer review:* *delegated to the PR reviewer (requester decision 2026-08-19); sign-off pending.* +*Second-engineer review:* Nathan Young, 2026-08-25, as the approving reviewer of PR #1717. ## Bugs found and fixed -This branch fixes all defects in this table. The fixes will be in the DREAM3D-NX release after v7.4.2. PR #1568 fixed `ErodeDilateMaskFilter-D2`, and PR #1717 fixed `ErodeDilateMaskFilter-D1`. +All defects in this table are fixed. The fixes will be in the DREAM3D-NX release after v7.4.2. PR #1568 fixed `ErodeDilateMaskFilter-D2`, and PR #1717 fixed `ErodeDilateMaskFilter-D1`. -| Deviation | Defect | Affected released versions | Resolution in this branch | +| Deviation | Defect | Affected released versions | Resolution | |-----------|--------|----------------------------|---------------------------| | `ErodeDilateMaskFilter-D1` | A non-positive iteration count returned success without changing the mask. | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was not affected. | Preflight rejects values below 1 with error `-14701`. | | `ErodeDilateMaskFilter-D2` | Disabled X, Y or Z directions still participated in morphology. | DREAM3D-NX v7.4.1 through v7.4.2. DREAM.3D v6.5.171 was not affected. | The algorithm applies each direction flag to its two face neighbours. | diff --git a/src/Plugins/SimplnxCore/vv/deviations/ErodeDilateMaskFilter.md b/src/Plugins/SimplnxCore/vv/deviations/ErodeDilateMaskFilter.md index ac2574777e..41d206f35e 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/ErodeDilateMaskFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/ErodeDilateMaskFilter.md @@ -1,6 +1,6 @@ # Deviations from DREAM.3D v6.5.171: ErodeDilateMaskFilter -This file has two parts. Part 1 records the historical validation difference from DREAM.3D v6.5.171 (D1). Part 2 records the fixed SIMPLNX direction-flag defect (D2). Both fixes are present in the current branch. +This file has two parts. Part 1 records the historical validation difference from DREAM.3D v6.5.171 (D1). Part 2 records the fixed SIMPLNX direction-flag defect (D2). Both fixes are in develop. Entries use stable IDs (`ErodeDilateMaskFilter-D`) from one sequence. The Filter UUID is the permanent cross-reference anchor. D1 keeps its original ID and retirement date; that date records the V&V decision, not a released fix. @@ -24,7 +24,7 @@ Entries use stable IDs (`ErodeDilateMaskFilter-D`) from one sequence. The Fil **Affected users:** Users whose scripts or saved pipelines supply an iteration count below 1. Earlier SIMPLNX execution silently skipped morphology. -**Recommendation:** Trust SIMPLNX on this branch. Supply at least one iteration; the filter now reports invalid counts before execution. The error code differs from DREAM.3D v6.5.171, but both reject the input. +**Recommendation:** Trust current SIMPLNX. Supply at least one iteration; the filter now reports invalid counts before execution. The error code differs from DREAM.3D v6.5.171, but both reject the input. **Evidence:** `SimplnxCore::ErodeDilateMaskFilter: Invalid Number of Iterations` checks invalid preflight and error `-14701` for 0 and -2. The requester chose the legacy validation behavior on 2026-08-19; no parameter key changed, and `parametersVersion()` remains 1. The zero-iteration comparison is recorded in `vv_work:ErodeDilateMaskFilter/ReadMe.md`, with inputs in `vv_work:ErodeDilateMaskFilter/extra/legacy_numiter0.json` and `vv_work:ErodeDilateMaskFilter/extra/nx_numiter0.d3dpipeline`. The test passes in CI at `01809c236`. All release branches from `upstream/release/v7.0.0` through `upstream/release/v7.4.2` omit the guard in `ErodeDilateMaskFilter.cpp`. @@ -48,10 +48,10 @@ Entries use stable IDs (`ErodeDilateMaskFilter-D`) from one sequence. The Fil **Affected users:** Users who disable an axis with valid face neighbours in DREAM3D-NX releases that contain the regression. All-directions-on examples cannot detect this defect. -**Recommendation:** Trust SIMPLNX on this branch. The algorithm now honors each selected axis. The release after v7.4.2 contains the correction. +**Recommendation:** Trust current SIMPLNX. The algorithm now honors each selected axis. The release after v7.4.2 contains the correction. **Evidence:** `Class 1 dilate honours direction flags` checks X-off, `Class 1 dilate with Y direction off` checks Y-off, and `Class 1 erode honours direction flags` checks Z-off. The current one-dimensional test also checks disabled-axis dilation and erosion on X and Z. These tests are in `src/Plugins/SimplnxCore/test/ErodeDilateMaskTest.cpp`. -**Release-source evidence:** `upstream/release/v7.0.0` through `upstream/release/v7.4.0` gate all six faces in `Filters/Algorithms/ErodeDilateMask.cpp`. `upstream/release/v7.4.1` and `upstream/release/v7.4.2` do not read the direction flags in that algorithm. DREAM.3D tag `v6.5.171`, `Source/Plugins/Processing/ProcessingFilters/ErodeDilateMask.cpp:227-250`, gates all six faces. Both fixes are present in this branch and will be in the DREAM3D-NX release after v7.4.2. +**Release-source evidence:** `upstream/release/v7.0.0` through `upstream/release/v7.4.0` gate all six faces in `Filters/Algorithms/ErodeDilateMask.cpp`. `upstream/release/v7.4.1` and `upstream/release/v7.4.2` do not read the direction flags in that algorithm. DREAM.3D tag `v6.5.171`, `Source/Plugins/Processing/ProcessingFilters/ErodeDilateMask.cpp:227-250`, gates all six faces. Both fixes are in develop and will be in the DREAM3D-NX release after v7.4.2. **Comparison evidence:** Run against DREAM.3D v6.5.171 on 2026-08-18: 28 of 28 mask cases match after the direction fix. See `vv_work:ErodeDilateMaskFilter/ReadMe.md` and `vv_work:ErodeDilateMaskFilter/results_compare.txt`. The direction-flag regression tests pass in CI at `01809c236`. diff --git a/src/Plugins/SimplnxCore/vv/provenance/6_6_erode_dilate_test.md b/src/Plugins/SimplnxCore/vv/provenance/6_6_erode_dilate_test.md index e193143ead..a4137017d2 100644 --- a/src/Plugins/SimplnxCore/vv/provenance/6_6_erode_dilate_test.md +++ b/src/Plugins/SimplnxCore/vv/provenance/6_6_erode_dilate_test.md @@ -11,9 +11,9 @@ This sidecar records the shared Erode/Dilate exemplar archive and the ErodeDilat | **Archive** | `6_6_erode_dilate_test.tar.gz` (68,857,947 bytes) | | **SHA512** | `5f0773e5d296936effbb2239965f5847e7c18533b0a2c3ec6a1d6a83b03417e5b459cce29808c8e0273613b3b6fa032c675e84926eb35d8da8a6ddc0641a0ef5` | | **Used by tests** | `SimplnxCore::ErodeDilateBadDataFilter(Erode)`; `SimplnxCore::ErodeDilateMaskFilter(Dilate)` and `(Erode)`; `SimplnxCore::ErodeDilateCoordinationNumberFilter` | -| **Generated by** | ** | -| **Generated on** | ** | -| **Generated at commit** | ** | +| **Generated by** | Michael Jackson (inferred from PR #439) | +| **Generated on** | 2023-02-07 to 2023-02-15 (inferred from PR #439) | +| **Generated at commit** | `98d2681e7` (PR #439 merge, inferred) | Read-only inspection on 2026-10-20 confirmed that the archive SHA512 matches `src/Plugins/SimplnxCore/test/CMakeLists.txt:234` and the value above. @@ -69,9 +69,8 @@ The current test source is identical to `01809c236`. All registered ErodeDilateM ## Second-engineer oracle review -- **Reviewer:** *pending* — required before the V&V report Status moves to COMPLETE -- **Date:** *pending* -- **Review focus when performed:** the two caveats above (later-than-baseline legacy version; all-directions-on coverage limit), and whether a `(Dilate)` production-scale test should be added from the unused exemplar already in the archive. +- **Reviewer:** Nathan Young, as the approving reviewer of PR #1717 +- **Date:** 2026-08-25 ## Regenerated to fix a circular-oracle situation? diff --git a/src/Plugins/SimplnxCore/vv/provenance/ComputeFeatureSizesFilter.md b/src/Plugins/SimplnxCore/vv/provenance/ComputeFeatureSizesFilter.md index 31d223768c..246f69dd99 100644 --- a/src/Plugins/SimplnxCore/vv/provenance/ComputeFeatureSizesFilter.md +++ b/src/Plugins/SimplnxCore/vv/provenance/ComputeFeatureSizesFilter.md @@ -9,6 +9,9 @@ This sidecar records the regression archive and the separate Class 1 fixtures us | **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`. @@ -353,7 +356,7 @@ Not applicable. The independent oracle is Class 1 and is encoded in the test sou ## Second-engineer oracle review -- **Reviewer:** Michael A. Jackson. +- **Reviewer:** Michael Jackson. - **Date:** 2026-07-28, as recorded in the report sign-off. ## Regenerated to fix a circular-oracle situation? From cdf53ad316396f2ef5e3da57a0ef9b6751ebc1ef Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 16:18:57 -0400 Subject: [PATCH 14/91] DOC: Link ComputeFeatureSizes-D3 to issue #1857 Signed-off-by: Michael Jackson --- src/Plugins/SimplnxCore/vv/ComputeFeatureSizesFilter.md | 2 +- .../SimplnxCore/vv/deviations/ComputeFeatureSizesFilter.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Plugins/SimplnxCore/vv/ComputeFeatureSizesFilter.md b/src/Plugins/SimplnxCore/vv/ComputeFeatureSizesFilter.md index f7c9044006..8e66381b01 100644 --- a/src/Plugins/SimplnxCore/vv/ComputeFeatureSizesFilter.md +++ b/src/Plugins/SimplnxCore/vv/ComputeFeatureSizesFilter.md @@ -20,7 +20,7 @@ | Tests today | 16 test cases: 14 visible and 2 hidden OOC cases. Coverage includes analytical values, output writes, invalid inputs, 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 | Source comparison with DREAM.3D v6.5.171 confirms D1–D3. Runtime comparison artifacts are not available. | -| Bug flags | `ComputeFeatureSizes-D3` | +| Bug flags | `ComputeFeatureSizes-D3` (issue #1857) | | V&V phase | Complete. `ComputeFeatureSizes-D3` remains open; its regression test is marked as an expected failure. | ## Summary diff --git a/src/Plugins/SimplnxCore/vv/deviations/ComputeFeatureSizesFilter.md b/src/Plugins/SimplnxCore/vv/deviations/ComputeFeatureSizesFilter.md index 81d001cb91..c1ba311647 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/ComputeFeatureSizesFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/ComputeFeatureSizesFilter.md @@ -56,7 +56,7 @@ Entries use the stable ID sequence `ComputeFeatureSizes-D`. The Filter UUID i | **Filter UUID** | `c666ee17-ca58-4969-80d0-819986c72485` | | **Status** | active | | **Affected released versions** | DREAM3D-NX v7.4.2. DREAM.3D v6.5.171 was not affected. | -| **Fixed in** | Not fixed. | +| **Fixed in** | Not fixed. Tracked in issue #1857. | **Symptom:** For a 2D image, the reported area includes the flat dimension's spacing, and the circular diameter uses that value. From a4fe16d426907fd7a645599da4909ca2d7c8a3f2 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 17:53:33 -0400 Subject: [PATCH 15/91] DOC: Rename the ComputeFeatureFaceMisorientation V&V files to match the filter class Signed-off-by: Michael Jackson --- ...rientations.md => ComputeFeatureFaceMisorientationFilter.md} | 2 +- ...rientations.md => ComputeFeatureFaceMisorientationFilter.md} | 0 ...rientations.md => ComputeFeatureFaceMisorientationFilter.md} | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename src/Plugins/OrientationAnalysis/vv/{ComputeFeatureFaceMisorientations.md => ComputeFeatureFaceMisorientationFilter.md} (99%) rename src/Plugins/OrientationAnalysis/vv/deviations/{ComputeFeatureFaceMisorientations.md => ComputeFeatureFaceMisorientationFilter.md} (100%) rename src/Plugins/OrientationAnalysis/vv/provenance/{ComputeFeatureFaceMisorientations.md => ComputeFeatureFaceMisorientationFilter.md} (100%) diff --git a/src/Plugins/OrientationAnalysis/vv/ComputeFeatureFaceMisorientations.md b/src/Plugins/OrientationAnalysis/vv/ComputeFeatureFaceMisorientationFilter.md similarity index 99% rename from src/Plugins/OrientationAnalysis/vv/ComputeFeatureFaceMisorientations.md rename to src/Plugins/OrientationAnalysis/vv/ComputeFeatureFaceMisorientationFilter.md index d50c4791fe..729c567db9 100644 --- a/src/Plugins/OrientationAnalysis/vv/ComputeFeatureFaceMisorientations.md +++ b/src/Plugins/OrientationAnalysis/vv/ComputeFeatureFaceMisorientationFilter.md @@ -29,7 +29,7 @@ ## Algorithm Relationship -*Classification:* **Rewrite** ~~| Port | Minor changes | New filter~~ +*Classification:* **Rewrite** *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. diff --git a/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFeatureFaceMisorientations.md b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFeatureFaceMisorientationFilter.md similarity index 100% rename from src/Plugins/OrientationAnalysis/vv/deviations/ComputeFeatureFaceMisorientations.md rename to src/Plugins/OrientationAnalysis/vv/deviations/ComputeFeatureFaceMisorientationFilter.md diff --git a/src/Plugins/OrientationAnalysis/vv/provenance/ComputeFeatureFaceMisorientations.md b/src/Plugins/OrientationAnalysis/vv/provenance/ComputeFeatureFaceMisorientationFilter.md similarity index 100% rename from src/Plugins/OrientationAnalysis/vv/provenance/ComputeFeatureFaceMisorientations.md rename to src/Plugins/OrientationAnalysis/vv/provenance/ComputeFeatureFaceMisorientationFilter.md From c4764fda0372b6a896cb3f92ad4e0721230eb8a4 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 19:00:21 -0400 Subject: [PATCH 16/91] DOC: Write V&V reports for users of a release, without Git terms A reader of a V&V report runs develop or a released DREAM3D-NX. Branch names, commit hashes, and PR numbers do not tell that reader whether their version has a defect. - Rename the "Resolution in this branch" column to "Resolution". - State fixes with DREAM3D-NX release versions only. - Add Git terms to the list of content that a report must not have. The Verified commit row and the provenance sidecar keep commit and PR evidence. Signed-off-by: Michael Jackson --- docs/vv_templates/deviation_template.md | 2 +- docs/vv_templates/report_gates.md | 2 +- docs/vv_templates/report_template.md | 13 +++++++------ 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/vv_templates/deviation_template.md b/docs/vv_templates/deviation_template.md index 9a53e742fe..014c4163f7 100644 --- a/docs/vv_templates/deviation_template.md +++ b/docs/vv_templates/deviation_template.md @@ -56,7 +56,7 @@ Entries are referenced by stable ID (`-D`) from the V&V report an | **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** | *PR #, DREAM3D-NX v* *or* *this branch, the release after v* *or* *Not fixed* | +| **Fixed in** | *DREAM3D-NX v* *or* *The DREAM3D-NX release after v* *or* *Not fixed* | **Symptom:** ** diff --git a/docs/vv_templates/report_gates.md b/docs/vv_templates/report_gates.md index c75f22ee13..ffa14bd36d 100644 --- a/docs/vv_templates/report_gates.md +++ b/docs/vv_templates/report_gates.md @@ -94,7 +94,7 @@ See [`defect_hunt.md`](./defect_hunt.md). - [ ] 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: the release after the latest affected release for this branch, or the named release for an earlier PR +- [ ] 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 diff --git a/docs/vv_templates/report_template.md b/docs/vv_templates/report_template.md index fd24e60d44..7be4c5d163 100644 --- a/docs/vv_templates/report_template.md +++ b/docs/vv_templates/report_template.md @@ -14,6 +14,7 @@ - Emoji, check marks, or strike-through text. - Any legacy version other than DREAM.3D v6.5.171. A comparison on a non-stock legacy build is "a local proof version". - A Legacy comparison result from a run on SIMPLNX code older than the current algorithm. Run the comparison again with a current `nxrunner` build when the algorithm changes. + - Git terms: branch names, "this branch", commit hashes, and PR numbers. A reader runs develop or a released DREAM3D-NX, so name DREAM3D-NX release versions instead. The Verified commit row is the only commit in a report. The provenance sidecar can cite commits and PRs as evidence. - The name "DREAM3D" for the legacy application. Write "DREAM.3D v6.5.171", or "DREAM.3D" for short. "DREAM3D-NX" is the new application. --> @@ -71,14 +72,14 @@ For a worked instance see `src/Plugins/SimplnxCore/vv/DBSCANFilter.md`. -*This branch fixes all defects in this table. The fixes will be in the DREAM3D-NX release after v.* +*All defects in this table are fixed. The fixes will be in the DREAM3D-NX release after v.* -*If an earlier PR fixed some of the defects, add:* *PR # fixed `-D`, and DREAM3D-NX v contains that fix.* +*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 in this branch | -|-----------|--------|----------------------------|---------------------------| -| `-D1` | *State the defect and its effect.* | *DREAM.3D v6.5.171; DREAM3D-NX v through v.* | *State how the verified branch fixes the defect.* | -| `-D2` | *State the defect and its effect.* | *DREAM.3D v6.5.171 only. DREAM3D-NX was not affected.* | *State how the verified branch fixes or prevents the defect.* | +| 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 From 6c54b03a43a768b0ef6cc3997e987e2982bd421d Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 19:01:01 -0400 Subject: [PATCH 17/91] DOC: Limit deviation entries to the template fields and an optional Evidence paragraph Signed-off-by: Michael Jackson --- docs/vv_templates/deviation_template.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/vv_templates/deviation_template.md b/docs/vv_templates/deviation_template.md index 014c4163f7..6b3aa96aee 100644 --- a/docs/vv_templates/deviation_template.md +++ b/docs/vv_templates/deviation_template.md @@ -4,6 +4,8 @@ This file has two parts. Part 1 lists every documented behavioral difference bet 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. + --- ## Part 1 — Differences from DREAM.3D v6.5.171 @@ -68,6 +70,8 @@ Entries are referenced by stable ID (`-D`) from the V&V report an **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) From fcdb7b9bd5faf0f6881d0a5997180be6e6cdaf36 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 19:10:57 -0400 Subject: [PATCH 18/91] DOC: Define the sign-off for V&V work that has no second-engineer approval Signed-off-by: Michael Jackson --- docs/vv_templates/report_template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/vv_templates/report_template.md b/docs/vv_templates/report_template.md index 7be4c5d163..4661d2e9bd 100644 --- a/docs/vv_templates/report_template.md +++ b/docs/vv_templates/report_template.md @@ -28,7 +28,7 @@ | Status | DRAFT *or* READY FOR REVIEW *or* COMPLETE | | Sign-off | *, YYYY-MM-DD; second engineer: , YYYY-MM-DD* | - + ## At a glance From c0a80d1adefea8f0c1a2451ef5fd92df04cc53f3 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 20:01:27 -0400 Subject: [PATCH 19/91] DOC: Rewrite the BadDataNeighborOrientationCheck V&V record to follow the report template Signed-off-by: Michael Jackson --- .../BadDataNeighborOrientationCheckFilter.md | 34 +++++++------ .../BadDataNeighborOrientationCheckFilter.md | 51 ++++++++----------- .../BadDataNeighborOrientationCheckFilter.md | 7 +-- 3 files changed, 44 insertions(+), 48 deletions(-) diff --git a/src/Plugins/OrientationAnalysis/vv/BadDataNeighborOrientationCheckFilter.md b/src/Plugins/OrientationAnalysis/vv/BadDataNeighborOrientationCheckFilter.md index e13b3bcf13..3ea52bf341 100644 --- a/src/Plugins/OrientationAnalysis/vv/BadDataNeighborOrientationCheckFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/BadDataNeighborOrientationCheckFilter.md @@ -15,9 +15,9 @@ | 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`. All 33 ordinary test cases pass in CI at `01809c236` on Linux, macOS and Windows. | -| Code paths enumerated | 17 of 27 exercised. Defensive guards, cancellation and I/O failures have no direct fixtures. | -| Tests today | 33 ordinary test cases and 2 hidden template variants cover masks, propagation, invariants, input validation and SIMPL conversion. | +| 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 | 17 of 28 exercised. One additional path has a fixture; ten paths have no direct fixtures. | +| Tests today | 34 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. | @@ -25,13 +25,13 @@ ## Summary -`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 branch contains the five documented defect fixes below. +`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:* Rewrite -*Evidence:* PR #1499 (`ebdce592f`) corrected the port; PR #1568 (`4ec21e93f`) replaced its repeated full-volume scans with Worklist and Scanline implementations. +*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 @@ -39,27 +39,27 @@ *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. -*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. All 33 ordinary test cases pass in CI at `01809c236`. +*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. *Second-engineer review:* Michael Jackson, 2026-06-01, as recorded in the sign-off. ## Bugs found and fixed -This branch fixes all defects in this table. PR #1499 fixed `BadDataNeighborOrientationCheckFilter-D1` and `BadDataNeighborOrientationCheckFilter-D2`, and DREAM3D-NX v7.4.1 contains those fixes. PR #1631 fixed `BadDataNeighborOrientationCheckFilter-D3` and `BadDataNeighborOrientationCheckFilter-D4`, and DREAM3D-NX v7.4.2 contains those fixes. PR #1568 fixed `BadDataNeighborOrientationCheckFilter-D5`; that fix will be in the DREAM3D-NX release after v7.4.2. +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`. -| Deviation | Defect | Affected released versions | Resolution in this branch | -|-----------|--------|----------------------------|---------------------------| +| 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. | Convert 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. | Reject invalid Laue indices with `-54901`; skip 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. | Skip nonpositive phases and return `-54902` for a participating phase beyond the ensemble array. | +| `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. | ## Code path coverage -17 of 27 paths exercised. The table maps source paths to existing fixtures. Each uncovered path states why it has no direct fixture. +17 of 28 paths exercised. Path 28 has an added fixture. The table maps source paths to existing fixtures and gives the reason for each path without a fixture. -Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/BadDataNeighborOrientationCheck.cpp` (37 lines), `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/BadDataNeighborOrientationCheckWorklist.cpp` (256 lines), `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/BadDataNeighborOrientationCheckScanline.cpp` (382 lines), and `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/BadDataNeighborOrientationCheckFilter.cpp` (173 lines). +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` (180 lines). Worklist counts matching neighbors and propagates changes through a queue. Scanline recounts neighbors in rolling slices until each level is stable. @@ -86,12 +86,13 @@ Worklist counts matching neighbors and propagates changes through a queue. Scanl | 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. Valid positive thresholds prevent rejected voxels from entering the queue; no nonpositive-threshold fixture exists.* | +| 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. No fixture uses a neighbor requirement above six.* | +| 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 | ## Test inventory @@ -125,6 +126,7 @@ Worklist counts matching neighbors and propagates changes through a queue. Scanl | `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. | diff --git a/src/Plugins/OrientationAnalysis/vv/deviations/BadDataNeighborOrientationCheckFilter.md b/src/Plugins/OrientationAnalysis/vv/deviations/BadDataNeighborOrientationCheckFilter.md index 679eb590b2..fe63688a2c 100644 --- a/src/Plugins/OrientationAnalysis/vv/deviations/BadDataNeighborOrientationCheckFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/deviations/BadDataNeighborOrientationCheckFilter.md @@ -15,16 +15,16 @@ Entries use stable IDs (`BadDataNeighborOrientationCheckFilter-D`) from one s | **Deviation ID** | `BadDataNeighborOrientationCheckFilter-D1` | | **Filter UUID** | `3f342977-aea1-49e1-a9c2-f73760eba0d3` | | **Status** | active | -| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.0. DREAM.3D v6.5.171 was affected. | -| **Fixed in** | PR #1499, DREAM3D-NX v7.4.1. | **Symptom:** A bad voxel can stay bad when its matching good-neighbor count equals the requested minimum. -**Root cause:** Bug. The DREAM.3D v6.5.171 source uses `while(currentLevel > m_NumberOfNeighbors)`. It omits the minimum level, including all propagation when the requested count is six. The same condition occurs in released SIMPLNX source. PR #1499 (`ebdce592f`) changes `>` to `>=`. Both current implementations retain the inclusive bound. See `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/BadDataNeighborOrientationCheckWorklist.cpp` and `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/BadDataNeighborOrientationCheckScanline.cpp`. +**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. -**Affected users:** Users whose bad voxels need propagation at the minimum neighbor level. A changed mask can change downstream results. +**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. -**Recommendation:** Trust SIMPLNX. The inclusive bound agrees with the parameter definition of a minimum number of neighbors. `Case 1.1.1` through `Case 1.6.1` check the minimum-level result. +**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. + +**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`. --- @@ -35,29 +35,16 @@ Entries use stable IDs (`BadDataNeighborOrientationCheckFilter-D`) from one s | **Deviation ID** | `BadDataNeighborOrientationCheckFilter-D2` | | **Filter UUID** | `3f342977-aea1-49e1-a9c2-f73760eba0d3` | | **Status** | active | -| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.0. DREAM.3D v6.5.171 was affected. | -| **Fixed in** | PR #1499, DREAM3D-NX v7.4.1. | **Symptom:** A different-phase neighbor can inherit a small angle from an earlier neighbor and incorrectly increase the matching-neighbor count. -**Root cause:** Bug. The DREAM.3D v6.5.171 source places the angle test outside the same-phase condition: - -```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) -{ - neighborCount[i]++; -} -``` +**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. -SIMPLNX release snapshots also contain this structure. PR #1499 (`ebdce592f`) puts the angle test inside the same-phase condition in both counting and propagation. Worklist retains that condition; Scanline uses `isMisorientationMatch()` to reject a phase mismatch before calculating the angle. The current rewrite changes the execution method but preserves this corrected rule and the D1 inclusive bound. The comparison isolates 37 extra good voxels caused by stale angles in a local proof version. Moving both angle tests inside the phase condition removes those differences. See `vv_work:BadDataNeighborOrientationCheckFilter/algorithm_review/source_comparison.md` and `vv_work:BadDataNeighborOrientationCheckFilter/comparison/legacy_emulator_results.md`. +**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. -**Affected users:** Users with mixed-phase neighbors. A stale angle can overcount matches at a phase boundary. +**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. -**Recommendation:** Trust SIMPLNX. Only a good neighbor of the same positive phase can count as a match. `Case 1.2.2` checks a mixed-phase expected mask. +**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`. --- @@ -71,16 +58,18 @@ SIMPLNX release snapshots also contain this structure. PR #1499 (`ebdce592f`) pu | **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** | PR #1631, DREAM3D-NX v7.4.2. | +| **Fixed in** | DREAM3D-NX v7.4.2 | **Symptom:** A rounded-up angle tolerance can admit a neighbor that should fail the strict boundary comparison. -**Root cause:** Bug. The earlier algorithm calculated radians with `float` and `numbers::pi_v`. PR #1631 (`ec22ac375`) replaces both with double precision. Worklist and Scanline retain that conversion. The current expected masks for `Case 1.3.3`, `Case 1.4.3`, `Case 1.5.3` and `Case 1.6.3` keep the center bad. DREAM.3D v6.5.171 also converts the tolerance with float arithmetic. The local proof comparison isolates four center-voxel differences in these cases and removes them with a corrected tolerance conversion. The stored float32 quaternions give an angle slightly above 5°; both tolerance conversion and angle rounding affect the decision. See `vv_work:BadDataNeighborOrientationCheckFilter/algorithm_review/source_comparison.md`. +**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. -**Affected users:** Users with misorientations sufficiently close to the strict tolerance boundary. The output effect depends on the orientation calculation as well as the tolerance conversion. +**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`. + --- ### BadDataNeighborOrientationCheckFilter-D4 @@ -91,16 +80,18 @@ SIMPLNX release snapshots also contain this structure. PR #1499 (`ebdce592f`) pu | **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** | PR #1631, DREAM3D-NX v7.4.2. | +| **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. PR #1631 (`ec22ac375`) adds a Laue-index check and a per-voxel guard. Both current implementations return `-54901` for an invalid non-sentinel value and skip a positive phase that resolves to `UnknownCrystalStructure`. No current fixture directly tests these rejection and skip paths. +**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. +**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`. + --- ### BadDataNeighborOrientationCheckFilter-D5 @@ -111,12 +102,14 @@ SIMPLNX release snapshots also contain this structure. PR #1499 (`ebdce592f`) pu | **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** | PR #1568, the DREAM3D-NX release after v7.4.2. | +| **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. The DREAM3D-NX v7.4.2 release snapshot reads `crystalStructures[cellPhases[voxelIndex]]` before checking the phase range. Earlier release branches and DREAM.3D v6.5.171 also read through an unchecked phase index. PR #1568 (`4ec21e93f`) checks the phase before the lookup in Worklist and Scanline. Nonpositive phases do not participate; a positive phase beyond the array returns `-54902`. `Phase Index Bounds` checks the bad-voxel error and permits an invalid phase on an already good voxel. +**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`. **Affected users:** Users with phase values that do not identify an entry in Crystal Structures on a bad voxel. **Recommendation:** Trust SIMPLNX. Correct the phase or supply the corresponding ensemble entry before processing that voxel. + +**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/provenance/BadDataNeighborOrientationCheckFilter.md b/src/Plugins/OrientationAnalysis/vv/provenance/BadDataNeighborOrientationCheckFilter.md index 546878cc53..cb4014ce1e 100644 --- a/src/Plugins/OrientationAnalysis/vv/provenance/BadDataNeighborOrientationCheckFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/provenance/BadDataNeighborOrientationCheckFilter.md @@ -1,4 +1,4 @@ -# Exemplar Archive Provenance: `bad_data_neighbor_orientation_check_v2.tar.gz` +# Exemplar Archive Provenance: bad_data_neighbor_orientation_check_v2.tar.gz This sidecar records the test-input archive, independent expected masks, release-source checks and legacy comparison for `BadDataNeighborOrientationCheckFilter`. @@ -15,7 +15,7 @@ This sidecar records the test-input archive, independent expected masks, release PR #1631 (`ec22ac375`) registers this archive name and SHA512 in `src/Plugins/OrientationAnalysis/test/CMakeLists.txt:173`. -The test source is identical to `01809c236`. All 33 ordinary test cases pass in CI at `01809c236` on Linux, macOS and Windows. The 2D, phase-bounds, tuple-count and HDF5 fixtures construct their inputs in C++. +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++. ## How it was generated @@ -59,7 +59,8 @@ Not applicable. The correctness oracles are Classes 1 and 4. Legacy outputs and ## Second-engineer oracle review -The report sign-off names Nathan Young (2026-02-02) and Michael Jackson (2026-06-01). Its Oracle text says that Michael Jackson reviewed the design. +- **Reviewer:** Michael Jackson, as recorded in the report. +- **Date:** 2026-06-01 ## Regenerated to fix a circular-oracle situation? From d3df8db9afa4c675be07bd841f95fa3babc4b421 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 20:01:27 -0400 Subject: [PATCH 20/91] DOC: Rewrite the CAxisSegmentFeatures V&V record to follow the report template Signed-off-by: Michael Jackson --- .../vv/CAxisSegmentFeaturesFilter.md | 233 ++++++++++-------- .../deviations/CAxisSegmentFeaturesFilter.md | 176 ++++++++++--- .../provenance/CAxisSegmentFeaturesFilter.md | 62 +++-- 3 files changed, 313 insertions(+), 158 deletions(-) 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/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/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. From 0a5ceed05dca191f81b5610ada9f09bfca988202 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 20:01:27 -0400 Subject: [PATCH 21/91] DOC: Rewrite the ComputeAvgCAxes V&V record to follow the report template Signed-off-by: Michael Jackson --- .../vv/ComputeAvgCAxesFilter.md | 232 +++++++----------- .../vv/deviations/ComputeAvgCAxesFilter.md | 164 ++++++------- .../vv/provenance/ComputeAvgCAxesFilter.md | 177 ++++++------- 3 files changed, 240 insertions(+), 333 deletions(-) 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/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/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. From ed034d98c04810f47714a3bc04c9c69770026468 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 20:01:27 -0400 Subject: [PATCH 22/91] DOC: Rewrite the ComputeAvgOrientations V&V record to follow the report template Signed-off-by: Michael Jackson --- .../vv/ComputeAvgOrientationsFilter.md | 231 +++++++++--------- .../ComputeAvgOrientationsFilter.md | 182 +++++++++++--- .../ComputeAvgOrientationsFilter.md | 115 +++++---- 3 files changed, 327 insertions(+), 201 deletions(-) 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/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/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. From a91458d5f04cdef7d800bb320669c8074cde042a Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 20:01:27 -0400 Subject: [PATCH 23/91] DOC: Rewrite the ComputeCAxisLocations V&V record to follow the report template Signed-off-by: Michael Jackson --- .../vv/ComputeCAxisLocationsFilter.md | 158 +++++++++--------- .../deviations/ComputeCAxisLocationsFilter.md | 66 +++++--- 2 files changed, 124 insertions(+), 100 deletions(-) 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/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. From eccdca31b4ff620510c97d0093b0134ce78aa867 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 20:01:27 -0400 Subject: [PATCH 24/91] DOC: Rewrite the ComputeFaceIPFColoring V&V record to follow the report template Signed-off-by: Michael Jackson --- .../vv/ComputeFaceIPFColoringFilter.md | 163 +++++++++--------- .../ComputeFaceIPFColoringFilter.md | 112 ++++++++---- ...teFaceIPFColoringFilter-circular-oracle.md | 47 +++-- 3 files changed, 194 insertions(+), 128 deletions(-) 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/deviations/ComputeFaceIPFColoringFilter.md b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFaceIPFColoringFilter.md index 0021feced8..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 backport in a locally patched legacy build `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)`. A locally patched legacy build 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. A locally patched legacy build contains the same fix (`1c96b3b8e`); SIMPLNX and that patched build 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 a locally patched legacy build): 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), the locally patched legacy build = `(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/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). From 5ed08f32d332a7a2c2e1e2e012934dc2852656be Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 20:01:27 -0400 Subject: [PATCH 25/91] DOC: Rewrite the ComputeFeatureFaceMisorientation V&V record to follow the report template Signed-off-by: Michael Jackson --- .../ComputeFeatureFaceMisorientationFilter.md | 218 ++++++------------ .../ComputeFeatureFaceMisorientationFilter.md | 133 ++++++----- .../ComputeFeatureFaceMisorientationFilter.md | 81 ++++--- 3 files changed, 189 insertions(+), 243 deletions(-) diff --git a/src/Plugins/OrientationAnalysis/vv/ComputeFeatureFaceMisorientationFilter.md b/src/Plugins/OrientationAnalysis/vv/ComputeFeatureFaceMisorientationFilter.md index 729c567db9..e157a80914 100644 --- a/src/Plugins/OrientationAnalysis/vv/ComputeFeatureFaceMisorientationFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/ComputeFeatureFaceMisorientationFilter.md @@ -1,179 +1,109 @@ # 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)* | +| | | +|--------|--------------| +| 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** — 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. | +| 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 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. +`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** +*Classification:* Rewrite -*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`. +*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)** 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). -- **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`. +*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. -### Second-engineer review +*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. -**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). +*Second-engineer review:* Michael Jackson, 2026-05-28, as recorded in the report sign-off. ## 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. +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 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. | +| 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 -*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 | +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 -| 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. | +The three ordinary test cases have the supplied Linux, macOS and Windows in-core CI evidence. All pass in CI. -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. +| 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 — 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`. +- **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` -### Historical downstream impact note +## Deviations from DREAM.3D v6.5.171 -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`. +See `vv/deviations/ComputeFeatureFaceMisorientationFilter.md` for the root cause, affected users and recommendation. D1–D4 retain their existing IDs. -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**. +| 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/deviations/ComputeFeatureFaceMisorientationFilter.md b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFeatureFaceMisorientationFilter.md index 046a366b36..1a0a18c8f7 100644 --- a/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFeatureFaceMisorientationFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFeatureFaceMisorientationFilter.md @@ -1,114 +1,113 @@ -# Deviations from DREAM3D 6.5.171: ComputeFeatureFaceMisorientationFilter +# Deviations from DREAM.3D v6.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). +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. --- -## ComputeFeatureFaceMisorientations-D1 +## Part 1 — Differences from DREAM.3D v6.5.171 + +### ComputeFeatureFaceMisorientations-D1 -| Field | Value | -|------------------|----------------------------------------| +| Field | Value | +|---|---| | **Deviation ID** | `ComputeFeatureFaceMisorientations-D1` | -| **Filter UUID** | `f3473af9-db77-43db-bd25-60df7230ea73` | -| **Status** | active | +| **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). +**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:** **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. +**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:** 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. +**Affected users:** Users with Hexagonal_Low, Cubic_Low, Triclinic, Monoclinic, OrthoRhombic, Tetragonal_Low, Tetragonal_High, Trigonal_Low or Trigonal_High phases. -**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. +**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 +### ComputeFeatureFaceMisorientations-D2 -| Field | Value | -|------------------|----------------------------------------| +| Field | Value | +|---|---| | **Deviation ID** | `ComputeFeatureFaceMisorientations-D2` | -| **Filter UUID** | `f3473af9-db77-43db-bd25-60df7230ea73` | -| **Status** | active | +| **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. -**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. 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. -**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). +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:** 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. +**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:** **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). +**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 +### ComputeFeatureFaceMisorientations-D3 -| Field | Value | -|------------------|----------------------------------------| +| Field | Value | +|---|---| | **Deviation ID** | `ComputeFeatureFaceMisorientations-D3` | -| **Filter UUID** | `f3473af9-db77-43db-bd25-60df7230ea73` | -| **Status** | active | +| **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). +**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:** **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: +**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. -| 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:** 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. -**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. Select faces with finite angles before calculating an aggregate that must exclude missing values. -**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. +**Evidence:** `Curated Data` checks the two background and two mixed-phase cases; unsupported Laue indices have no direct fixture. --- -## ComputeFeatureFaceMisorientations-D4 +### ComputeFeatureFaceMisorientations-D4 -| Field | Value | -|------------------|----------------------------------------| +| Field | Value | +|---|---| | **Deviation ID** | `ComputeFeatureFaceMisorientations-D4` | -| **Filter UUID** | `f3473af9-db77-43db-bd25-60df7230ea73` | -| **Status** | active | +| **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. +**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. -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. 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. -**Root cause:** **Precision** in EbsdLib's `CubicOps::calculateMisorientationInternal` (NOT in this filter's code). The algorithm computed `cos(half-angle)` candidates as: +DREAM.3D v6.5.171 uses float intermediates and `acos` in `Source/OrientationLib/LaueOps/CubicOps.cpp`. This calculation loses precision near zero. -```cpp -double w_candidate_2 = (qco.z() + qco.w()) / sqrt(2); // type-2 sym op -``` +**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. -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: +**Recommendation:** Trust SIMPLNX. DREAM3D-NX v7.4.2 contains the fix. The filter uses stable cubic angle extraction near zero. -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. +**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°. -**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. +## Part 2 — SIMPLNX defects and behavior changes -**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. +### ComputeFeatureFaceMisorientationFilter-D5 -**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. +| 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. -## Comparison build & library nuance +**Recommendation:** Trust SIMPLNX. Correct an invalid participating phase or supply its ensemble entry. -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. +**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/provenance/ComputeFeatureFaceMisorientationFilter.md b/src/Plugins/OrientationAnalysis/vv/provenance/ComputeFeatureFaceMisorientationFilter.md index cc5714819a..5f8824f500 100644 --- a/src/Plugins/OrientationAnalysis/vv/provenance/ComputeFeatureFaceMisorientationFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/provenance/ComputeFeatureFaceMisorientationFilter.md @@ -1,59 +1,76 @@ # 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. - ---- +This sidecar records the inline analytical fixtures and the archived legacy comparison for `ComputeFeatureFaceMisorientationFilter`. ## 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) | +| 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 -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. +`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. -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). +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. -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). +`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. -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. +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. -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. +`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. -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. +| 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° | -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. +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. -The dataset is inlined in `src/Plugins/OrientationAnalysis/test/ComputeFeatureFaceMisorientationTest.cpp` namespace `curated` (function `CreateTestDataStructure()`). +`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 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). +| 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) -N/A — Class 1 oracle. +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:** *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.* +- **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? -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.) +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. From d19afd93a7dcb39d06bee40e43e9560816bc94ce Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 20:01:27 -0400 Subject: [PATCH 26/91] DOC: Rewrite the ComputeFeatureNeighborCAxisMisalignments V&V record to follow the report template Signed-off-by: Michael Jackson --- ...FeatureNeighborCAxisMisalignmentsFilter.md | 219 +++++--------- ...FeatureNeighborCAxisMisalignmentsFilter.md | 222 +++++--------- ...FeatureNeighborCAxisMisalignmentsFilter.md | 283 +++++------------- 3 files changed, 240 insertions(+), 484 deletions(-) diff --git a/src/Plugins/OrientationAnalysis/vv/ComputeFeatureNeighborCAxisMisalignmentsFilter.md b/src/Plugins/OrientationAnalysis/vv/ComputeFeatureNeighborCAxisMisalignmentsFilter.md index 311b942d9a..e8ecda6234 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 | Not run — comparison artifacts are not available. | +| 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/deviations/ComputeFeatureNeighborCAxisMisalignmentsFilter.md b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFeatureNeighborCAxisMisalignmentsFilter.md index 0b9d77b8f8..90a5750c5d 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. --- -## 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. --- -## 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 fixture directly selects Hexagonal_Low. --- -## 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/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. From 9db59e695b935309231e73c3c39778541a9f1932 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 20:01:27 -0400 Subject: [PATCH 27/91] DOC: Rewrite the ComputeFeatureNeighborMisorientations V&V record to follow the report template Signed-off-by: Michael Jackson --- ...uteFeatureNeighborMisorientationsFilter.md | 195 +++++++----------- ...uteFeatureNeighborMisorientationsFilter.md | 91 ++++---- ...uteFeatureNeighborMisorientationsFilter.md | 143 +++++-------- 3 files changed, 161 insertions(+), 268 deletions(-) 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/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/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. From 1576dc42bddcf0992afa1713d334e25ca4d7a213 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 20:01:27 -0400 Subject: [PATCH 28/91] DOC: Rewrite the ComputeFeatureReferenceCAxisMisorientations V&V record to follow the report template Signed-off-by: Michael Jackson --- ...tureReferenceCAxisMisorientationsFilter.md | 215 +++++++----------- ...tureReferenceCAxisMisorientationsFilter.md | 147 +++++------- ...tureReferenceCAxisMisorientationsFilter.md | 202 ++++++---------- 3 files changed, 209 insertions(+), 355 deletions(-) diff --git a/src/Plugins/OrientationAnalysis/vv/ComputeFeatureReferenceCAxisMisorientationsFilter.md b/src/Plugins/OrientationAnalysis/vv/ComputeFeatureReferenceCAxisMisorientationsFilter.md index a40e939d34..b9f95dbc20 100644 --- a/src/Plugins/OrientationAnalysis/vv/ComputeFeatureReferenceCAxisMisorientationsFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/ComputeFeatureReferenceCAxisMisorientationsFilter.md @@ -1,166 +1,123 @@ # 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 | 16 of 27 exercised by the visible tests. Gaps include Hexagonal-Low, angles above 90°, transfer failures and cancellation. | +| Tests today | 9 test cases: 8 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 retains the legacy non-hexagonal correction and fixes two SIMPLNX defects. ## 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. - +16 of 27 paths exercised. The count uses the eight visible tests; the table lists the remaining paths and their gaps. + +Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeFeatureReferenceCAxisMisorientations.cpp` (277 lines); `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeFeatureReferenceCAxisMisorientationsFilter.cpp` (198 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, not the exact code | +| 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 | *Not directly tested. No analytical fixture selects Hexagonal-Low.* | +| 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 or negative FeatureId → skip the angle calculation | *Not directly tested. The skip fixtures use a negative phase or feature zero.* | +| 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 by FeatureId | All three Class 1 cases | +| 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 | *Not directly tested. The only fixture for this path is the hidden `genuine HDF5 slice-boundary oracle`.* | +| 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 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.* | ## 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 +The eight visible cases are in `src/Plugins/OrientationAnalysis/test/ComputeFeatureReferenceCAxisMisorientationsTest.cpp`. All pass in CI. The table also includes the hidden case. -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. +| 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. | ## 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/deviations/ComputeFeatureReferenceCAxisMisorientationsFilter.md b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFeatureReferenceCAxisMisorientationsFilter.md index eaa5208ee2..b439950dfb 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 remains float32. Its variance accumulator is double, although subtraction of the stored cell angle and feature mean occurs in float32 before assignment to double. -| 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/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. From 597af38ec80bff10e8e8ad8b6becffad1985bbef Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 20:01:27 -0400 Subject: [PATCH 29/91] DOC: Rewrite the ComputeFeatureReferenceMisorientations V&V record to follow the report template Signed-off-by: Michael Jackson --- ...teFeatureReferenceMisorientationsFilter.md | 208 ++++++++---------- ...teFeatureReferenceMisorientationsFilter.md | 159 ++++++++++--- ...teFeatureReferenceMisorientationsFilter.md | 145 +++++------- 3 files changed, 272 insertions(+), 240 deletions(-) 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/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/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. From b704214fee571debfe451531c3c99b1745f47065 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 20:01:27 -0400 Subject: [PATCH 30/91] DOC: Rewrite the ComputeIPFColors V&V record to follow the report template Signed-off-by: Michael Jackson --- .../vv/ComputeIPFColorsFilter.md | 178 +++++++++--------- .../vv/deviations/ComputeIPFColorsFilter.md | 66 ++++++- .../vv/provenance/ComputeIPFColorsFilter.md | 64 +++++++ 3 files changed, 209 insertions(+), 99 deletions(-) create mode 100644 src/Plugins/OrientationAnalysis/vv/provenance/ComputeIPFColorsFilter.md 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/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/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. From 2a8837cbc8d26f1fb5b6d838205bad2c3edda3ee Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 20:01:27 -0400 Subject: [PATCH 31/91] DOC: Rewrite the ComputeKernelAvgMisorientations V&V record to follow the report template Signed-off-by: Michael Jackson --- .../ComputeKernelAvgMisorientationsFilter.md | 247 ++++++++--------- .../ComputeKernelAvgMisorientationsFilter.md | 188 +++++-------- .../ComputeKernelAvgMisorientationsFilter.md | 252 ++++++++---------- 3 files changed, 295 insertions(+), 392 deletions(-) diff --git a/src/Plugins/OrientationAnalysis/vv/ComputeKernelAvgMisorientationsFilter.md b/src/Plugins/OrientationAnalysis/vv/ComputeKernelAvgMisorientationsFilter.md index 8ddb464875..c5d448c007 100644 --- a/src/Plugins/OrientationAnalysis/vv/ComputeKernelAvgMisorientationsFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/ComputeKernelAvgMisorientationsFilter.md @@ -1,162 +1,151 @@ # 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. +28 of 45 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` (175 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.* | ## 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. | ## 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/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/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. From 613de6b9c804104e7636868091a0849a9756faa7 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 20:01:27 -0400 Subject: [PATCH 32/91] DOC: Rewrite the ComputeSchmids V&V record to follow the report template Signed-off-by: Michael Jackson --- .../vv/ComputeSchmidsFilter.md | 205 ++++++------------ .../vv/deviations/ComputeSchmidsFilter.md | 153 +++++++------ 2 files changed, 140 insertions(+), 218 deletions(-) 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/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. From 7a81ba5c4b8e184bc668a8c7f7dd0c0238c2293c Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 20:01:27 -0400 Subject: [PATCH 33/91] DOC: Rewrite the ConvertOrientations V&V record to follow the report template Signed-off-by: Michael Jackson --- .../vv/ConvertOrientationsFilter.md | 159 +++++++++--------- .../deviations/ConvertOrientationsFilter.md | 86 +++++----- 2 files changed, 128 insertions(+), 117 deletions(-) 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/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. From c2f506b4ccc100c21876ce0af18d08bbe6f9a39d Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 20:01:28 -0400 Subject: [PATCH 34/91] DOC: Rewrite the GroupMicroTextureRegions V&V record to follow the report template Signed-off-by: Michael Jackson --- .../vv/GroupMicroTextureRegionsFilter.md | 138 +++++------ .../GroupMicroTextureRegionsFilter.md | 217 ++++++++---------- .../GroupMicroTextureRegionsFilter.md | 137 +++++------ 3 files changed, 235 insertions(+), 257 deletions(-) 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/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/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. From 129c6c5b53982b238b137512ea6a7fb787b3b976 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 20:01:28 -0400 Subject: [PATCH 35/91] DOC: Rewrite the NeighborOrientationCorrelation V&V record to follow the report template Signed-off-by: Michael Jackson --- .../NeighborOrientationCorrelationFilter.md | 239 +++++++++--------- .../NeighborOrientationCorrelationFilter.md | 149 ++++++++--- ...hborOrientationCorrelationFilter_inputs.md | 39 +++ .../neighbor_orientation_correlation_v2.md | 109 ++++---- 4 files changed, 333 insertions(+), 203 deletions(-) create mode 100644 src/Plugins/OrientationAnalysis/vv/provenance/NeighborOrientationCorrelationFilter_inputs.md diff --git a/src/Plugins/OrientationAnalysis/vv/NeighborOrientationCorrelationFilter.md b/src/Plugins/OrientationAnalysis/vv/NeighborOrientationCorrelationFilter.md index 00e6491449..a087eab449 100644 --- a/src/Plugins/OrientationAnalysis/vv/NeighborOrientationCorrelationFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/NeighborOrientationCorrelationFilter.md @@ -1,152 +1,153 @@ # 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 | 26 of 40 exercised. Gaps include transfer-order cases, cancellation, and I/O failures. | +| Tests today | 19 ordinary test cases cover analytical outputs, invariants, invalid inputs and SIMPL conversion. All pass in CI; 1 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. +26 of 40 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` (243 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.* | ## Test inventory +All names are in `src/Plugins/OrientationAnalysis/test/NeighborOrientationCorrelationTest.cpp`. The 19 ordinary cases are the CI validation set. All pass in CI. 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. | ## 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/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/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/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`. From 1f4c0377e4124508263a9d94cb7e50ddc09fac26 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 20:01:28 -0400 Subject: [PATCH 36/91] DOC: Rewrite the ReadAngData V&V record to follow the report template Signed-off-by: Michael Jackson --- .../vv/ReadAngDataFilter.md | 193 ++++++++---------- .../vv/deviations/ReadAngDataFilter.md | 97 ++++++--- .../vv/provenance/ReadAngDataFilter.md | 80 ++++++-- 3 files changed, 210 insertions(+), 160 deletions(-) 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/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/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. From f8f98ac83a1d6181d4770dcf4f48dde5e3c3130d Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 20:01:28 -0400 Subject: [PATCH 37/91] DOC: Rewrite the ReadCtfData V&V record to follow the report template Signed-off-by: Michael Jackson --- .../vv/ReadCtfDataFilter.md | 222 ++++++++---------- .../vv/deviations/ReadCtfDataFilter.md | 131 +++++++---- .../vv/provenance/ReadCtfDataFilter.md | 72 ++++-- 3 files changed, 232 insertions(+), 193 deletions(-) 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/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/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`. From 0ec689bdd6411e7d7db966d11c033b7ade04b083 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 20:01:28 -0400 Subject: [PATCH 38/91] DOC: Rewrite the ReadEbsdPatternFile V&V record to follow the report template Signed-off-by: Michael Jackson --- .../vv/ReadEbsdPatternFileFilter.md | 115 +++++++++++------- .../deviations/ReadEbsdPatternFileFilter.md | 7 +- 2 files changed, 76 insertions(+), 46 deletions(-) diff --git a/src/Plugins/OrientationAnalysis/vv/ReadEbsdPatternFileFilter.md b/src/Plugins/OrientationAnalysis/vv/ReadEbsdPatternFileFilter.md index 605f6143cc..7c6e7468ff 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. | +| 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 — comparison artifacts are not available. 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/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. From 6c0f0d19badea60fa3a4fa8a52db11f6a1c33c2e Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 20:01:28 -0400 Subject: [PATCH 39/91] DOC: Rewrite the ReadH5OinaData V&V record to follow the report template Signed-off-by: Michael Jackson --- .../vv/ReadH5OinaDataFilter.md | 258 ++++++------- .../vv/deviations/ReadH5OinaDataFilter.md | 345 +++++++++++------- .../vv/provenance/ReadH5OinaDataFilter.md | 307 ++++++---------- 3 files changed, 443 insertions(+), 467 deletions(-) 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/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/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. From 7b68f025068091cf3a5228c7ff017ea280eb47b6 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 20:01:28 -0400 Subject: [PATCH 40/91] DOC: Rewrite the RotateEulerRefFrame V&V record to follow the report template Signed-off-by: Michael Jackson --- .../vv/RotateEulerRefFrameFilter.md | 125 +++++++++--------- .../deviations/RotateEulerRefFrameFilter.md | 71 ++++------ .../provenance/RotateEulerRefFrameFilter.md | 120 +++++++++-------- 3 files changed, 156 insertions(+), 160 deletions(-) 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/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/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. From 31f199db1c3b169ef2a7b40a97cb1550be7b276c Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 20:01:28 -0400 Subject: [PATCH 41/91] DOC: Rewrite the WritePoleFigure V&V record to follow the report template Signed-off-by: Michael Jackson --- .../vv/WritePoleFigureFilter.md | 192 +++++++++++------- .../vv/deviations/WritePoleFigureFilter.md | 162 ++++++++++++--- .../vv/provenance/WritePoleFigureFilter.md | 51 ++--- 3 files changed, 274 insertions(+), 131 deletions(-) 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/WritePoleFigureFilter.md b/src/Plugins/OrientationAnalysis/vv/deviations/WritePoleFigureFilter.md index f4feedc5d0..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, a locally patched legacy build, and SIMPLNX (EbsdLib 3.1.0). The patched legacy build's hex-Ti outputs matched 6.5.171 byte-for-byte. 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/WritePoleFigureFilter.md b/src/Plugins/OrientationAnalysis/vv/provenance/WritePoleFigureFilter.md index 115f633a12..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**, **a locally patched legacy build**, 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 / locally patched legacy build / NX × color/discrete × hex/cubic) — hex: `out_legacy_6_5_171/{color,discrete}_Phase_1.pdf`, the patched legacy build output (matched 6.5.171 byte-for-byte), `out_nx/nx_{color,discrete}_1.png`; cubic: `out_cubic_legacy_6_5_171/`, the patched legacy build output (matched 6.5.171 except for a trailing PDF metadata block in color mode; discrete output matched byte-for-byte), `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. From 2297d7e0e5229b28c2c1e7ac4d0514f1cf48dca8 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 20:01:28 -0400 Subject: [PATCH 42/91] DOC: Rewrite the ComputeFeatureCentroids V&V record to follow the report template Signed-off-by: Michael Jackson --- .../vv/ComputeFeatureCentroidsFilter.md | 197 +++++++----------- .../ComputeFeatureCentroidsFilter.md | 78 +++++-- .../ComputeFeatureCentroidsFilter.md | 64 +++--- 3 files changed, 171 insertions(+), 168 deletions(-) diff --git a/src/Plugins/SimplnxCore/vv/ComputeFeatureCentroidsFilter.md b/src/Plugins/SimplnxCore/vv/ComputeFeatureCentroidsFilter.md index 3cac3febea..8316deef5a 100644 --- a/src/Plugins/SimplnxCore/vv/ComputeFeatureCentroidsFilter.md +++ b/src/Plugins/SimplnxCore/vv/ComputeFeatureCentroidsFilter.md @@ -1,165 +1,116 @@ # 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 | 16 of 26 exercised by the five ordinary test cases. The table identifies validation, cancellation, I/O and periodic-axis gaps. | +| Tests today | 6 test cases: analytical centroids, periodic centroids, invalid IDs, bulk reads, SIMPL conversion and a hidden storage-contract fixture. | +| Exemplar archive | None. The tests construct inputs and expected values in C++. | +| Legacy comparison | Not run — comparison artifacts are not available. | +| 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. +16 of 26 paths exercised. The count uses the five ordinary CI 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` (165 lines), and `src/simplnx/Utilities/DataArrayUtilities.cpp` (515 lines; shared FeatureIds 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.* | ## Test inventory +The five ordinary test cases run in in-core CI on Linux, macOS and Windows. All pass in CI. + | 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. | ## 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 `FindFeatureCentroids.cpp` in DREAM3D 6.5.171 and a locally patched legacy build 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/deviations/ComputeFeatureCentroidsFilter.md b/src/Plugins/SimplnxCore/vv/deviations/ComputeFeatureCentroidsFilter.md index 35726fa3e1..dd7075c27b 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/ComputeFeatureCentroidsFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/ComputeFeatureCentroidsFilter.md @@ -1,14 +1,12 @@ -# Deviations from DREAM3D 6.5.171: ComputeFeatureCentroidsFilter +# Deviations from DREAM.3D v6.5.171: ComputeFeatureCentroidsFilter -This file lists every documented behavioral difference between the SIMPLNX `ComputeFeatureCentroidsFilter` and its DREAM3D 6.5.171 equivalent `FindFeatureCentroids` (SIMPL UUID `6f8ca36f-2995-5bd3-8672-6b0b80d5b2ca`, `Source/Plugins/Generic/GenericFilters/FindFeatureCentroids.{h,cpp}`). - -Entries are referenced by stable ID (`ComputeFeatureCentroidsFilter-D`) from the V&V report and public migration guidance. The Filter UUID field is the permanent cross-reference anchor. - -**Algorithm Relationship:** *Port.* The non-periodic path is a near line-by-line translation of `FindFeatureCentroids::find_centroids()`, preserving the identical Kahan compensated summation and `double` accumulators. Comparison basis: **source inspection of both implementations** (`FindFeatureCentroids.cpp` in DREAM3D 6.5.171 and a locally patched legacy build vs SIMPLNX `Algorithms/ComputeFeatureCentroids.cpp`), backed by the Class 1 analytical fixtures in `test/ComputeFeatureCentroidsTest.cpp` that verify SIMPLNX against hand-derived values independently of either legacy build. An empirical three-way binary A/B (6.5.171 PipelineRunner + a locally patched legacy build containing the backport + nxrunner) can be run to bit-confirm D1 if the SBIR deliverable requires it; source analysis is conclusive for a verified port. +Part 1 records the coordinate-precision difference and the added periodic mode. Part 2 records the two fixed SIMPLNX periodic-centroid defects. Both parts use the stable `ComputeFeatureCentroidsFilter-D` sequence. --- -## ComputeFeatureCentroidsFilter-D1 +## Part 1 — Differences from DREAM.3D v6.5.171 + +### ComputeFeatureCentroidsFilter-D1 | Field | Value | |---|---| @@ -16,17 +14,19 @@ Entries are referenced by stable ID (`ComputeFeatureCentroidsFilter-D`) from | **Filter UUID** | `c6875ac7-8bdd-4f69-b6ce-82ac09bd3421` | | **Status** | active | -**Symptom:** Centroid components may differ from DREAM3D 6.5.171 in the lowest bit(s) of the `float32` result on features with large voxel counts and/or large coordinate magnitudes. On typical data the outputs are bit-identical. +**Symptom:** Non-periodic centroid components can differ from DREAM.3D v6.5.171 because the implementations use different coordinate precision. + +**Root cause:** Precision. Both implementations use double-precision Kahan sums and store float32 centroids. DREAM.3D v6.5.171 reads each cell center into `std::array` before accumulation. SIMPLNX calculates `origin + (index + 0.5) * spacing` with double-precision operands in `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeFeatureCentroids.cpp`. -**Root cause:** *Precision.* Both implementations accumulate voxel-center coordinates into `double` with the identical Kahan compensated-summation kernel and store the final centroid as `float32`. The only difference is how the per-voxel center is fetched before accumulation: SIMPLNX computes it directly in `float64` (`ImageGeom::getCoords` → `Point3Dd`), whereas 6.5.171 computes it into a `float[3]` and then promotes to `double` inside the Kahan step (`FindFeatureCentroids.cpp:161,175–199`). SIMPLNX therefore carries full double precision through the whole accumulation; legacy carries a `float32`-rounded coordinate into each add. After averaging and the final cast back to `float32`, the residual difference is at or below one `float32` ULP of the coordinate value. +**Affected users:** Users who require identical floating-point centroid values when migrating from DREAM.3D v6.5.171. -**Affected users:** Anyone requiring bit-exact reproducibility of centroid values across the DREAM3D → DREAM3D-NX migration, on datasets with very large coordinate magnitudes or features spanning millions of voxels. Users who visualize or do downstream morphology at `float32` precision will not observe a difference. +**Recommendation:** Trust SIMPLNX. Its coordinate calculation avoids the earlier float32 rounding; compare numerical results with a tolerance appropriate to the data. -**Recommendation:** *Trust SIMPLNX.* The `float64` voxel-center path is strictly more accurate; 6.5.171 is not more correct — it simply discards precision earlier. No legacy patch needed. +**Evidence:** `Source/Plugins/Generic/GenericFilters/FindFeatureCentroids.cpp` at the DREAM.3D `v6.5.171` tag contains the float coordinate buffer and Kahan kernel. The current SIMPLNX algorithm contains explicit double-precision coordinate expressions. Both sources divide each accumulated coordinate by its cell count. --- -## ComputeFeatureCentroidsFilter-D2 +### ComputeFeatureCentroidsFilter-D2 | Field | Value | |---|---| @@ -34,18 +34,58 @@ Entries are referenced by stable ID (`ComputeFeatureCentroidsFilter-D`) from | **Filter UUID** | `c6875ac7-8bdd-4f69-b6ce-82ac09bd3421` | | **Status** | active | -**Symptom:** SIMPLNX exposes an `Is Periodic` option that, when enabled, offsets the centroid of any feature spanning the full extent of an axis (wrap-aware centroid). DREAM3D 6.5.171 `FindFeatureCentroids` has no such option and always produces the literal (truncated) centroid. +**Symptom:** Enabling `Is Periodic` can change centroid components for features that touch both ends of an image axis. DREAM.3D v6.5.171 always calculates arithmetic means. -**Root cause:** *Algorithmic choice (feature addition).* The `Is Periodic` parameter and the periodic centroid computation are SIMPLNX-only. Legacy has no periodic path, so there is nothing to diff against — this is an additive capability, not a divergence on shared behavior. The SIMPLNX default is `false`, which reproduces legacy exactly. SIMPL 6.4/6.5 pipelines convert with `Is Periodic` defaulted to `false`, so auto-converted legacy pipelines match 6.5.171. +**Root cause:** Algorithmic choice. SIMPLNX adds a circular mean for each spanning axis. A normalized unit-vector resultant below `1e-6` preserves the arithmetic mean. `Is Periodic` defaults to false, including after SIMPL pipeline conversion. With periodic mode off, the precision difference in D1 still applies. -**Affected users:** Only users who explicitly enable `Is Periodic`. Auto-converted legacy pipelines are unaffected (default `false`). +**Affected users:** Users who enable `Is Periodic`. Automatic conversion of a DREAM.3D pipeline does not enable it. -**Recommendation:** *Trust SIMPLNX.* Opt-in feature with a legacy-preserving default. +**Recommendation:** Trust SIMPLNX. Enable periodic mode for periodic geometry; keep it disabled for ordinary arithmetic centroids. D3 and D4 describe corrections to this added mode. -**Note (SIMPLNX-internal bugs fixed during V&V — not legacy deviations):** The periodic centroid had two SIMPLNX-internal defects, both fixed. (1) *Spacing:* the offset in the `ImageGeom` overload of `AdjustCentroidsForPeriodicFaces` was added in cell-index units (`(dim−1)/2`) to a physical-coordinate centroid, correct only when spacing == 1 (fixed during #1658). (2) *Model (issue #1665, 2026-07 follow-up):* the constant half-domain offset is itself only correct for a feature whose mass is symmetric about the seam, and can place asymmetric wrapped features outside the domain. The periodic path was reimplemented as a **minimum-image circular mean** accumulated during the cell sweep, and the range-based `ImageGeom` overload (insufficient inputs to compute a wrapped centroid) was removed. `test/ComputeFeatureCentroidsTest.cpp` Fixtures D/E (independent minimum-image oracle) and F (domain-filling fallback) pin the corrected behavior. Because 6.5.171 has no periodic path, neither fix is a change relative to legacy; recorded here for any SIMPLNX user who ran `Is Periodic` before the fixes. +**Evidence:** `ComputeFeatureCentroidsFilter::parameters()` defines the false default, and `FromSIMPLJson()` starts from default arguments. DREAM.3D `v6.5.171` `FindFeatureCentroids` has no periodic parameter or calculation. Fixtures D–F in `src/Plugins/SimplnxCore/test/ComputeFeatureCentroidsTest.cpp` check the periodic behavior independently. --- -## Retracted candidate deviations +## Part 2 — SIMPLNX defects and behavior changes + +### ComputeFeatureCentroidsFilter-D3 + +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeFeatureCentroidsFilter-D3` | +| **Filter UUID** | `c6875ac7-8bdd-4f69-b6ce-82ac09bd3421` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.2.1 through v7.4.1. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | + +**Symptom:** With non-unit spacing, the periodic centroid offset used cell-index units instead of physical units. + +**Root cause:** Bug. The image overload of `AdjustCentroidsForPeriodicFaces` added `(dimension - 1) / 2` directly to a physical centroid. DREAM3D-NX v7.4.2 multiplies that offset by the axis spacing. The current circular-mean calculation also includes spacing in cell coordinates and domain lengths. DREAM.3D v6.5.171 has no periodic code path. + +**Affected users:** Users who enable periodic mode with non-unit spacing on a spanning axis in the affected releases. + +**Recommendation:** Trust SIMPLNX. DREAM3D-NX v7.4.2 corrects the spacing units, but D4 still affects its periodic results; the release after v7.4.2 contains both corrections. + +**Evidence:** Direct inspection of `ComputeFeatureCentroids.cpp` and `src/simplnx/Utilities/GeometryHelpers.cpp` on all ten release branches shows no periodic mode through v7.2.0, an unscaled offset from v7.2.1 through v7.4.1, and the scaled offset in v7.4.2. PR #1658 (`ded8bf33f`) applies the spacing correction. Current fixture E checks physical X = 11 with spacing 2 and origin 10 after both D3 and D4 corrections. + +--- + +### ComputeFeatureCentroidsFilter-D4 + +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeFeatureCentroidsFilter-D4` | +| **Filter UUID** | `c6875ac7-8bdd-4f69-b6ce-82ac09bd3421` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.2.1 through v7.4.2. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | The DREAM3D-NX release after v7.4.2 | + +**Symptom:** The periodic mode could return an incorrect centroid for an asymmetric wrapped feature or place the centroid outside the image domain. + +**Root cause:** Bug. A constant half-span offset cannot represent the distribution of a feature's cells across a periodic boundary. The current calculation accumulates cell-center unit vectors, converts their mean angle to a physical coordinate and retains the arithmetic mean when the resultant is below `1e-6`. DREAM.3D v6.5.171 has no periodic code path. + +**Affected users:** Users who enable periodic mode in the affected releases, especially for asymmetric wrapped features or features that fill the domain. + +**Recommendation:** Trust SIMPLNX. Use the DREAM3D-NX release after v7.4.2 for the corrected periodic calculation. Disable periodic mode only when an ordinary arithmetic centroid is appropriate. -- **~~D: Kahan-vs-naive summation precision~~** — The retroactive DRAFT report (`docs/vv_retroactive_reports/ComputeFeatureCentroidsFilter.md`) hypothesized that SIMPLNX added Kahan summation over a legacy *naive* running sum. **Retracted:** source inspection shows 6.5.171 `FindFeatureCentroids::find_centroids()` already uses the identical Kahan kernel with identical `double` accumulators. No such deviation exists; the residual precision difference is captured by D1 (voxel-center fetch width), not summation method. +**Evidence:** The periodic implementation in `upstream/release/v7.2.1` through `upstream/release/v7.4.2` calls the constant-offset image helper. PR #1670 (`4a7b71e5f`) replaces that model with the circular mean and removes the image helper. Current fixtures D and E independently unwrap coordinates to obtain X = 3.5 and X = 11; fixture F requires the arithmetic fallback X = 2. diff --git a/src/Plugins/SimplnxCore/vv/provenance/ComputeFeatureCentroidsFilter.md b/src/Plugins/SimplnxCore/vv/provenance/ComputeFeatureCentroidsFilter.md index 3f89a4b4ce..2db308d66f 100644 --- a/src/Plugins/SimplnxCore/vv/provenance/ComputeFeatureCentroidsFilter.md +++ b/src/Plugins/SimplnxCore/vv/provenance/ComputeFeatureCentroidsFilter.md @@ -1,39 +1,51 @@ -# Exemplar Provenance: ComputeFeatureCentroidsFilter +# Exemplar Archive Provenance: ComputeFeatureCentroidsFilter -**No exemplar archive.** As of this V&V cycle (2026-07-07) the filter's correctness tests are **inline Class 1 (Analytical) + Class 4 (Invariant) fixtures** built directly in `test/ComputeFeatureCentroidsTest.cpp` (`namespace CentroidToy`). There is no `.dream3d` gold-master to hash — the oracle lives in the test code as hand-derived `REQUIRE` values. +The filter uses inline analytical fixtures. It consumes no exemplar archive. -## Retired test / circular-oracle removal +## Archive identity -The prior test `SimplnxCore::ComputeFeatureCentroidsFilter` compared a freshly computed `Centroids NX` array against a **sibling `Centroids` array in the same `6_6_stats_test_v2.dream3d` file**. That reference array was itself produced by an earlier DREAM3D run, making the check a **consistency-with-self / circular oracle** (see `docs/vv_templates/oracle_classes.md` "What is NOT an oracle"). It was **retired** and replaced by the analytical fixtures below. +| Field | Value | +|---|---| +| **Archive** | None | +| **SHA512** | Not applicable; no archive is consumed by these tests. | +| **Used by tests** | Inline fixtures in `src/Plugins/SimplnxCore/test/ComputeFeatureCentroidsTest.cpp` | +| **Generated by** | Not applicable; `CentroidToy::Build()` constructs the inputs. | +| **Generated on** | Not applicable; inputs are constructed when the tests run. | +| **Generated at commit** | Not applicable to an archive. Analytical fixtures were added in `ded8bf33f` (PR #1658); periodic expectations were corrected in `4a7b71e5f` (PR #1670). | -- `6_6_stats_test_v2.tar.gz` — the centroids test no longer consumes it. The `download_test_data()` entry stays in `test/CMakeLists.txt` because five other tests still use it (`ComputeEuclideanDistMap`, `ComputeSurfaceAreaToVolume`, `ComputeFeatureNeighbors`, `ComputeFeatureSizes`, `ComputeFeaturePhases`). -- `6_6_find_feature_centroids.tar.gz` — **kept**; used by `ExtractComponentAsArray` and `WriteAbaqusHexahedron` tests (the retroactive report's "orphan" claim was incorrect). +## How it was generated -## Canonical oracle output (Class 1 — hand derivation) +`CentroidToy::Build()` creates an image, explicit FeatureIds and a feature AttributeMatrix from each fixture's dimensions, spacing and origin. Expected values are hand-derived constants in `RequireCentroid()` calls. The bulk fixture constructs 65,537 cells with non-unit spacing and nonzero origin; its failure variant supplies a store that rejects its second bulk read. -Centroid component = `Σ(voxel-center coord) / N` over the feature's cells, where `voxel-center = origin + (index + 0.5)·spacing`. Fixtures (all in `test/ComputeFeatureCentroidsTest.cpp`): +## Canonical oracle output -| Fixture | Grid / spacing | Purpose | Key expected value | -|---|---|---|---| -| A | 3×1×1 | basic mean + empty (count==0) background | centroid[1] = (1.5, 0.5, 0.5) | -| B | 4×2×1 | multi-feature, single-cell, empty id | centroid[1] = (5/6, 5/6, 0.5); centroid[2] = (3.0, 1.0, 0.5) | -| C | 2×2×2 | 3D z-stride | centroid[1] = (1,1,0.5); centroid[2] = (1,1,1.5) | -| D | 4×1×1 (spacing 1) | periodic wrap fires on spanning feature only | centroid[1] = 2.16667 + 1.5 = 3.66667 | -| E | 4×1×1 (spacing 2, origin 10) | periodic offset scales with spacing (regression pin for the D2 bug fix) | centroid[1].x = 17.0 (was 15.5 before fix) | -| (error) | 2×1×1, id 5 of 2 | validation error path (-5351) | execute invalid | +| DataPath | Source of expected values | +|---|---| +| `/Image/FeatureData/Centroids` | Class 1 arithmetic means or independently unwrapped periodic coordinates, with Class 4 unchanged-feature and fallback checks. | -## Second-engineer oracle review +| Fixture | Construction | Expected centroid or X component | +|---|---|---| +| A | 3×1×1, one feature | Feature 1: `(1.5, 0.5, 0.5)`; empty feature 0: `(0, 0, 0)` | +| B | 4×2×1, three occupied features | Feature 1: `(5/6, 5/6, 0.5)`; feature 2: `(3, 1, 0.5)`; feature 3: `(1.5, 1.5, 0.5)` | +| C | 2×2×2, one feature per Z plane | Feature 1: `(1, 1, 0.5)`; feature 2: `(1, 1, 1.5)` | +| D | 4×1×1; feature 1 at X centers 0.5, 2.5, 3.5 | Unwrap 0.5 to 4.5: mean X = 3.5; non-spanning feature 2 stays at X = 1.5 | +| E | 4×1×1; X spacing 2, X origin 10; feature centers 11, 13, 17 | Unwrap 11 and 13 to 19 and 21: mean 19 maps to X = 11 in the domain `[10,18)` | +| F | 4×1×1; one feature fills the image | Unit vectors cancel on X; arithmetic fallback X = 2 | +| Bulk | 65,537×1×1; spacing `(2,3,4)`, origin `(10,-2,1)` | Features 1 and 2: `(65547, -0.5, 3)`; empty feature 0: `(0,0,0)` | -- **Reviewer:** Michael Jackson (technical authority) -- **Date:** 2026-07-16 -- Review focus: the Fixture E spacing-aware periodic value (17.0) and the D2 bug-fix rationale. +The five ordinary test cases use inputs built in C++ or the two SIMPL conversion JSON files. All pass in CI at `419f9db35` on Linux, macOS and Windows (in-core builds). The current algorithm, test file and validation helper match that source revision. + +The second injected bulk read occurs in `ValidateFeatureIdsToFeatureAttributeMatrixIndexing()`, which reads 40,000 IDs at a time. It returns `-91911` before the algorithm's 65,536-cell accumulation loop starts. The test checks the error and a zero output component; it does not exercise a failure inside the accumulation loop. + +## Oracle provenance (Classes 2, 3, 5 only) -## Reproduction +Not applicable. Classes 1 and 4 use expectations in the test source. The DREAM.3D v6.5.171 source comparison establishes the shared Kahan kernel and the coordinate-width difference; it does not supply expected test values. -The fixtures require no external data. Build the `SimplnxCoreUnitTest` target and run: +## Second-engineer oracle review + +- **Reviewer:** Michael Jackson (recorded technical-authority review) +- **Date:** 2026-07-16 -``` -ctest -R "ComputeFeatureCentroids" -``` +## Regenerated to fix a circular-oracle situation? -All hand derivations are in the code comments beside each `RequireCentroid` call and in `src/Plugins/SimplnxCore/vv/ComputeFeatureCentroidsFilter.md` (Phase 5). +No archive was regenerated. PR #1658 removed the comparison with the sibling `Centroids` array in `6_6_stats_test_v2.dream3d` and added inline analytical expectations. PR #1670 replaced constant-offset periodic expectations with the independent unwrapped-coordinate derivations in fixtures D and E and added fixture F. The current test file contains no exemplar loader or `TestFileSentinel`. From 9503851e49d759745d23ae5739854e18b5960d3a Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 20:01:28 -0400 Subject: [PATCH 43/91] DOC: Rewrite the ComputeFeatureNeighbors V&V record to follow the report template Signed-off-by: Michael Jackson --- .../vv/ComputeFeatureNeighborsFilter.md | 214 ++++++++++-------- .../ComputeFeatureNeighborsFilter.md | 83 ++----- .../ComputeFeatureNeighborsFilter.md | 105 +++++---- 3 files changed, 207 insertions(+), 195 deletions(-) diff --git a/src/Plugins/SimplnxCore/vv/ComputeFeatureNeighborsFilter.md b/src/Plugins/SimplnxCore/vv/ComputeFeatureNeighborsFilter.md index a6be2777ab..6093f0becd 100644 --- a/src/Plugins/SimplnxCore/vv/ComputeFeatureNeighborsFilter.md +++ b/src/Plugins/SimplnxCore/vv/ComputeFeatureNeighborsFilter.md @@ -1,118 +1,146 @@ # 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 | 24 of 34 exercised by the mapped tests. Error returns, cancellation, negative IDs and the unreachable dispatch fallback have no direct fixture. | +| Tests today | 38 test cases: 35 analytical cases, one archive regression, one SIMPL conversion 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 | Not run — comparison artifacts are not available. | +| 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 | +24 of 34 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` (223 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.* | ## 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 37 ordinary test cases cover analytical expectations, archive regression and conversion. All 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. | ## Exemplar archive @@ -120,9 +148,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/deviations/ComputeFeatureNeighborsFilter.md b/src/Plugins/SimplnxCore/vv/deviations/ComputeFeatureNeighborsFilter.md index 7ef645ef7a..f3dbe6fc7a 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/ComputeFeatureNeighborsFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/ComputeFeatureNeighborsFilter.md @@ -1,88 +1,49 @@ -# Deviations from DREAM3D 6.5.171: ComputeFeatureNeighborsFilter +# Deviations from DREAM.3D v6.5.171: ComputeFeatureNeighborsFilter -This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent (`FindNeighbors`, SIMPL UUID `97cf66f8-7a9b-5ec2-83eb-f8c4c8a17bac`, `Source/Plugins/Statistics/StatisticsFilters/FindNeighbors.cpp`). +Part 1 lists the two documented differences from `FindNeighbors` in DREAM.3D v6.5.171. Both defects also affected DREAM3D-NX v7.0.0 through v7.4.1. There are no Part 2 entries. -Legacy source reviewed at: `DREAM3D/Source/Plugins/Statistics/StatisticsFilters/FindNeighbors.cpp`. +The deviation IDs are stable. The Filter UUID is the permanent cross-reference anchor. -Comparison run on the `6_6_stats_test_v2.tar.gz` SmallIn100 fixture (SHA512 `e84999dec914d81efce4fc4237c49c9bf32e48381b1e79f58aa4df934f0d7606cd7a948f9a5e7b17a126a7944cc531b531cfdc70756ca3e2207b20734e089723`). +## Part 1 — Differences from DREAM.3D v6.5.171 ---- - -## ComputeFeatureNeighborsFilter-D1 +### ComputeFeatureNeighborsFilter-D1 | Field | Value | |---|---| | **Deviation ID** | `ComputeFeatureNeighborsFilter-D1` | | **Filter UUID** | `7177e88c-c3ab-4169-abe9-1fdaff20e598` | -| **Status** | active — **A/B-proven (2026-06-29)** | - -**Symptom:** `SharedSurfaceAreaList` values produced by SIMPLNX differ from DREAM3D 6.5.171 for any dataset with anisotropic spacing. The test at `test/ComputeFeatureNeighborsTest.cpp:910` records this explicitly: *"The exemplar Shared Surface Area is not valid after a bug fix, and the input file is used in other test cases."* - -**Root cause:** Bug in 6.5.171. The legacy finalization loop at `FindNeighbors.cpp:431`: +| **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 | -```cpp -float area = float(number) * xRes * yRes; -``` +**Symptom:** Shared surface areas can differ from DREAM.3D v6.5.171 when voxel spacing is anisotropic. -computes the shared surface area between a feature pair as `count × xSpacing × ySpacing`, where `count` is the number of shared face-boundary voxel-contacts. This formula is only correct for faces whose normal points along Z (those faces have area = xSpacing × ySpacing). For faces with X-normal (area = ySpacing × zSpacing) or Y-normal (area = xSpacing × zSpacing), the formula gives the wrong result whenever spacing is anisotropic. +**Root cause:** Bug. `FindNeighbors.cpp` counts contacts and multiplies every count by X spacing times Y spacing. This is the Z-normal face area. An X-normal face needs Y spacing times Z spacing; a Y-normal face needs X spacing times Z spacing. Current SIMPLNX accumulates each contact with the area returned by `computeFaceSurfaceAreas()` in `src/simplnx/Utilities/NeighborUtilities.hpp`. -SIMPLNX corrects this via `computeFaceSurfaceAreas()` (`NeighborUtilities.hpp:322`), which returns a per-face-direction area array: `{sx·sy, sx·sz, sy·sz, sy·sz, sx·sz, sx·sy}` for faces `{−Z, −Y, −X, +X, +Y, +Z}`. The accumulation `neighborSurfaceAreas[feature][neighborFeatureId] += precomputedFaceAreas[faceIndex]` therefore applies the geometrically correct area for each individual face contact. +**Affected users:** Users with anisotropic spacing and contacts whose face areas differ from X spacing times Y spacing. Downstream calculations that use SharedSurfaceAreaList can change. Isotropic spacing hides the error. -**Affected users:** All workflows using `SharedSurfaceAreaList` on datasets with anisotropic voxel spacing (xSpacing ≠ ySpacing ≠ zSpacing). For isotropic spacing (all three equal), the legacy formula happens to give the correct result for all face directions and no deviation is observable. The Small IN100 dataset has anisotropic spacing; downstream filters that consume `SharedSurfaceAreaList` (e.g., `ComputeSlipTransmissionMetrics`) will produce different results. +**Recommendation:** Trust SIMPLNX. DREAM3D-NX v7.4.2 and later use the geometric area of each shared face. The analytical 2D and 3D fixtures check direction-specific areas independently of the archive. -**Recommendation:** Trust SIMPLNX. The 6.5.171 formula is geometrically incorrect for any non-Z-normal face when spacing is anisotropic. The SIMPLNX values are verified against the geometric definition of face area by 37 independent Class 1 analytical test cases. - -**A/B verification (2026-06-29):** A 2×2×2 image with anisotropic spacing (1,2,3) split along X into two features (shared faces are X-normal, area = sy·sz = 6, four contacts) was run through stock 6.5.171, SIMPLNX, and a local build of the legacy source with a surgical fix applied. Stock 6.5.171 produced `SharedSurfaceAreaList = [8, 8]` (4 · sx·sy = 8 — the buggy formula); SIMPLNX produced `[24, 24]` (4 · 6). A legacy-source patch that accumulates the correct per-face-direction area during the scan (`faceArea = {sx·sy, sx·sz, sy·sz, sy·sz, sx·sz, sx·sy}`, k = 0..5) reproduced SIMPLNX's `[24, 24]` exactly, while leaving NumNeighbors, NeighborList, SurfaceFeatures, and BoundaryCells byte-identical. On isotropic spacing all three agree, confirming the deviation is spacing-dependent as stated. +**Evidence:** The DREAM.3D `v6.5.171` source at `Source/Plugins/Statistics/StatisticsFilters/FindNeighbors.cpp` contains `float area = float(number) * xRes * yRes`. Each `upstream/release/v7.0.0` through `upstream/release/v7.4.1` source contains the same formula. The v7.4.2 source contains per-face accumulation. PR #1569 (`9e61b41d4`) introduced the correction; the release-source table is in `../provenance/ComputeFeatureNeighborsFilter.md`. --- -## ComputeFeatureNeighborsFilter-D2 +### ComputeFeatureNeighborsFilter-D2 | Field | Value | |---|---| | **Deviation ID** | `ComputeFeatureNeighborsFilter-D2` | | **Filter UUID** | `7177e88c-c3ab-4169-abe9-1fdaff20e598` | -| **Status** | active — **A/B-proven (2026-06-29)** | - -**Symptom:** `SurfaceFeatures` in DREAM3D 6.5.171 marks every feature as a surface feature for any image whose X or Y dimension is 1 (i.e., 1D images and 2D EmptyY/EmptyX images). SIMPLNX correctly identifies only features that touch the actual image boundary in the active dimensions. - -**Root cause:** Bug in 6.5.171. The legacy surface-feature check at `FindNeighbors.cpp:330–343`: +| **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 | -```cpp -// Branch 1 — fires when ZPoints != 1 -if((column == 0 || column == XPoints-1 || row == 0 || row == YPoints-1 || - plane == 0 || plane == ZPoints-1) && ZPoints != 1) -{ - m_SurfaceFeatures[feature] = true; -} -// Branch 2 — fires when ZPoints == 1 (EmptyZ 2D) -if((column == 0 || column == XPoints-1 || row == 0 || row == YPoints-1) && ZPoints == 1) -{ - m_SurfaceFeatures[feature] = true; -} -``` +**Symptom:** DREAM.3D v6.5.171 marks interior features as surface features on 1D images and on 2D images with a unit X or Y dimension. -Branch 2 correctly handles the EmptyZ case (ZPoints==1) by restricting the check to X and Y boundaries. However, Branch 1 is used for all other cases including 1D images and 2D EmptyY/EmptyX images: +**Root cause:** Bug. The legacy boundary condition omits Z when Z has length one, but always tests X and Y. With unit X or Y, every cell has column zero or row zero, so every positive feature is marked. Current Direct execution distinguishes corners, edges and faces by dimensionality. Current Scanline execution tests boundaries only on axes longer than one and treats a single voxel as a surface feature. -- **1D Z image** (XPoints=1, YPoints=1, ZPoints=N): `column == 0` is always true (only one X column), so every voxel triggers Branch 1 → every feature is marked surface. -- **1D Y image** (XPoints=1, YPoints=N, ZPoints=1): `column == 0` is always true → Branch 2 fires → every feature is marked surface. -- **1D X image** (XPoints=N, YPoints=1, ZPoints=1): `row == 0` is always true (only one Y row) → Branch 2 fires → every feature is marked surface. -- **2D EmptyY image** (XPoints=M, YPoints=1, ZPoints=N, ZPoints≠1): `row == 0` is always true → Branch 1 fires → every feature is marked surface. -- **2D EmptyX image** (XPoints=1, YPoints=M, ZPoints=N, ZPoints≠1): `column == 0` is always true → Branch 1 fires → every feature is marked surface. - -SIMPLNX's correction: explicit dimensionality dispatch to `ImageDimensionStateT` specializations. The `ProcessSurfaceFeaturesV` template flag is applied at three levels: corner processing (marks all dimensionalities unconditionally for corners, which are always on the boundary), edge processing (`!Is1DImageDimsState()` guard — edges are only on the boundary for 2D and 3D), and face processing (`std::is_same_v` guard — boundary-plane interior voxels only exist in 3D). This correctly restricts surface-feature marking to voxels that genuinely touch the image boundary in each active dimension. - -**Affected users:** Any pipeline that runs `ComputeFeatureNeighbors` / `FindNeighbors` on a 2D or 1D image geometry (`XPoints==1` or `YPoints==1`) and uses the `SurfaceFeatures` output. For full 3D datasets (all three dimensions > 1), the two implementations produce identical `SurfaceFeatures` output. This deviation is **not observable** on the SmallIn100 dataset (all three dimensions ≫ 1), which is why the SmallIn100 legacy comparison test passes for SurfaceFeatures. - -**Recommendation:** Trust SIMPLNX. The 6.5.171 result is geometrically incorrect for degenerate image dimensions — it flags internal features as surface features when a dimension-1 axis makes `column==0` or `row==0` trivially true for every voxel. - -**A/B verification (2026-06-29):** A 5×1×5 EmptyY image (Y=1, Z>1) with an interior single-voxel feature (feature 2 at the center, surrounded by feature 1) was run through stock 6.5.171, SIMPLNX, and a local build of the legacy source with a surgical fix applied. Stock 6.5.171 produced `SurfaceFeatures = [0, 1, 1]` — wrongly marking the interior feature 2 as surface (Branch 1 fires because `row==0` is trivially true for every voxel when Y=1); SIMPLNX produced `[0, 1, 0]`. A legacy-source dimensionality-aware boundary check (only the extremes of non-degenerate dims count; an all-degenerate image is treated as boundary) reproduced SIMPLNX's `[0, 1, 0]` exactly, while leaving all other arrays byte-identical. On a full 3D image all three agree, confirming the deviation is confined to degenerate (X=1 or Y=1) geometry as stated. - ---- +**Affected users:** Users who enable SurfaceFeatures on 1D images or unit-X or unit-Y 2D images. Full 3D images and 2D images with only a unit Z dimension do not expose this defect. -## Non-deviations (documented for future-engineer awareness) +**Recommendation:** Trust SIMPLNX. DREAM3D-NX v7.4.2 and later mark only features that touch the boundary in an active dimension. The 1D and 2D analytical fixtures assert false flags for interior features. -- **NumNeighbors** — bit-identical on SmallIn100. Both implementations deduplicate face contacts via a map-like structure (`QMap` in legacy; `std::map` in SIMPLNX) and write the size as `NumNeighbors`. -- **NeighborList** — bit-identical on SmallIn100. Both produce entries in ascending neighbor feature-ID order (legacy via `QMap` iteration; SIMPLNX via `std::map` iteration). This ordering is an algorithm characteristic, not a guaranteed API contract. -- **BoundaryCells** — not present in the `6_6_stats_test_v2.tar.gz` archive; comparison not run on SmallIn100. By inspection, both implementations count `onsurf` / `numDiffNeighbors` identically: increment once per valid face contact where the neighbor has a different non-zero feature ID. The face-validity logic (boundary guard per face index) is equivalent in both for all dimensionalities — the legacy `neighpoints` + per-index plane/row/column guards correctly exclude invalid faces for all geometry shapes, matching the SIMPLNX `computeValidFaceNeighbors` output. Correctness of SIMPLNX BoundaryCells established by the Class 1 oracle fixtures. -- **Neighbor ordering** — same `QMap` / `std::map` ascending-key iteration order in both; algorithm characteristic, not a specified API guarantee. +**Evidence:** The tagged `FindNeighbors.cpp` source tests `column == 0` and `row == 0` in both boundary conditions. Each `upstream/release/v7.0.0` through `upstream/release/v7.4.1` source retains those conditions. The v7.4.2 source uses dimensionality-specific marking. Current tests `Case 1.0.0`, `Case 1.1.0`, `Case 1.2.0`, `Case 2.1.0` and `Case 2.2.0` assert the corrected flags. diff --git a/src/Plugins/SimplnxCore/vv/provenance/ComputeFeatureNeighborsFilter.md b/src/Plugins/SimplnxCore/vv/provenance/ComputeFeatureNeighborsFilter.md index 8e39e6e527..50ec16c35a 100644 --- a/src/Plugins/SimplnxCore/vv/provenance/ComputeFeatureNeighborsFilter.md +++ b/src/Plugins/SimplnxCore/vv/provenance/ComputeFeatureNeighborsFilter.md @@ -1,4 +1,6 @@ -# Exemplar Provenance: ComputeFeatureNeighborsFilter +# Exemplar Archive Provenance: 6_6_stats_test_v2.tar.gz + +This sidecar records the shared archive, separate analytical fixtures and release-source evidence for `ComputeFeatureNeighborsFilter`. ## Archive identity @@ -6,26 +8,54 @@ |---|---| | **Archive** | `6_6_stats_test_v2.tar.gz` | | **SHA512** | `e84999dec914d81efce4fc4237c49c9bf32e48381b1e79f58aa4df934f0d7606cd7a948f9a5e7b17a126a7944cc531b531cfdc70756ca3e2207b20734e089723` | -| **Used by** | `Legacy: SmallIn100` test only; archive also consumed by other statistics-filter tests | -| **Generated by** | BlueQuartz Software; legacy DREAM3D 6.5.171/6.6.x pipeline on Small IN100 dataset | -| **Generated on** | Unknown (pre-SIMPLNX V&V era; `6_6_` prefix indicates DREAM3D 6.6.x era) | +| **Used by tests** | `SimplnxCore::ComputeFeatureNeighborsFilter: Legacy: SmallIn100` | +| **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 serves as a **legacy comparison input only**. All correctness oracles are the inline hand-derived fixtures described below. +The first registration of this archive and SHA512 is in PR #1124. The current declaration is `src/Plugins/SimplnxCore/test/CMakeLists.txt:248`. ---- +## How it was generated -## Inline test fixture analysis +PR #1124 replaced the shared statistics archive after correcting the voxel-center calculation in ComputeShapes. The neighbor regression loads `6_6_stats_test_v2.dream3d`, selects Small IN100 FeatureIds and compares three archived arrays. The independent analytical inputs and expectations are constructed in `src/Plugins/SimplnxCore/test/ComputeFeatureNeighborsTest.cpp` by the single-voxel, 1D, square 2D, non-square 2D and 3D builders. -All 33 structured test cases (Cases 0–3) use inline expected values embedded directly in `test/ComputeFeatureNeighborsTest.cpp`. No archived oracle outputs are used. The derivation methodology is: +The 11 analytical geometry fixtures expand to 35 ordinary cases. Eight dimensionality states use four optional-output combinations each; three non-square fixtures use both optional outputs. The ordinary inventory also includes one archive regression and one SIMPL conversion case. All pass in CI at `419f9db35` on Linux, macOS and Windows (in-core builds). -- **BoundaryCells[v]**: count of face contacts where the neighboring voxel has a distinct, non-zero feature ID. -- **SurfaceFeatures[f]**: `true` for any feature with at least one voxel on the image boundary in the active dimensions. -- **NumNeighbors[f] / NeighborList[f]**: count and sorted (ascending) list of distinct features sharing at least one face with feature `f`. -- **SharedSurfaceAreaList (SSA)[f]**: total shared face area per neighbor. Face area is the product of the two spacings perpendicular to the face normal: Z-normal → `sx·sy`; Y-normal → `sx·sz`; X-normal → `sy·sz`. +**Release-source evidence** ---- +The sources below were read at every listed release. Algorithm paths are relative to `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/`. + +| Releases | Source | D1: shared area | D2: surface flags | +|---|---|---|---| +| DREAM3D-NX v7.0.0 through v7.4.0 | `ComputeFeatureNeighborsFilter.cpp` | Contact count times X spacing times Y spacing | Unit X or Y marks all positive features | +| DREAM3D-NX v7.4.1 | `Algorithms/ComputeFeatureNeighbors.cpp` | Same formula | Same boundary conditions | +| DREAM3D-NX v7.4.2 | `Algorithms/ComputeFeatureNeighbors.cpp` and `src/simplnx/Utilities/NeighborUtilities.hpp` | Per-face area | Boundary contact in active dimensions | + +The inspected refs are `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`. DREAM.3D v6.5.171 was affected by D1 and D2: its tagged `Source/Plugins/Statistics/StatisticsFilters/FindNeighbors.cpp` contains the same area formula and unit-axis boundary conditions. PR #1569 (`9e61b41d4`) changed the neighbor algorithm; PR #1590 (`e76ad508e`) moved dimensionality operations into shared utilities. + +**Comparison record** + +PR #1641 (`01c9602b1`) records a comparison dated 2026-06-29. It records shared areas `[8,8]` for DREAM.3D v6.5.171 and `[24,24]` for SIMPLNX on a 2×2×2 X-split with spacing (1,2,3). It also records surface flags `[0,1,1]` and `[0,1,0]` on a 5×1×5 image with an interior feature. A local proof version reproduced the SIMPLNX values. These values explain D1 and D2; the report does not use this record as a current comparison result. + +The algorithm-file history after that date contains `01c9602b1` (2026-06-30, moves a tuple-count query into the 3D path), `ab55798d4` (2026-08-21, formatting), `4ec21e93f` (2026-09-20, Direct/Scanline split), and `a33c370ca` (2026-09-25, messaging API). Shared utility `NeighborUtilities.hpp` also changed in `ba246bc7c` (2026-09-24). + +## Canonical oracle output -### Fixture 0: Single voxel +The archive supplies regression references, not the independent correctness oracle. The archive test compares `DataContainer/CellFeatureData/SurfaceFeatures`, `NumNeighbors` and `NeighborList`. It does not compare BoundaryCells or SharedSurfaceAreaList. + +The canonical analytical expectations live in the test code: + +| DataPath | Source of expected values | +|---|---| +| `Image/CellData/ExemplarBoundaryCells` | Class 1 count of faces touching a different positive feature | +| `Image/FeatureData/ExemplarSurfaceFeatures` | Class 1 boundary contact in active dimensions | +| `Image/FeatureData/ExemplarNumNeighbors` | Class 1 count of distinct positive neighbor IDs | +| `Image/FeatureData/ExemplarNeighborsList` | Class 1 sorted distinct neighbor IDs | +| `Image/FeatureData/ExemplarSharedSurfaceAreaList` | Class 1 sum of direction-specific face areas for each feature pair | + +`ExecuteFilter()` compares the three required outputs and each requested optional output. The fixtures use zero origin; translation does not change adjacency or face area. Unequal spacings distinguish face directions, and the non-square 2D fixtures distinguish row strides. + +**Fixture 0: Single voxel** **Source:** `CreateSingleVoxelDataStructure()` — geometry 1×1×1, spacing (1.0, 1.0, 1.0), one feature (FeatureIds[0]=1). @@ -39,7 +69,7 @@ All 33 structured test cases (Cases 0–3) use inline expected values embedded d --- -### Fixture 1: 1D (Z, Y, X variants) +**Fixture 1: 1D (Z, Y, X variants)** **Source:** `Create1DZDataStructure()`, `Create1DYDataStructure()`, `Create1DXDataStructure()` @@ -70,21 +100,21 @@ FeatureIds (linear): `[1, 2, 2, 2, 3, 0, 4]` | 3 | false | 1 | {2} | {1.0} | | 4 | true (at dim-end boundary) | 0 | {} | {} | -**SSA note:** The 2.2 spacing is along the active axis and does not contribute to face area. The two inactive dimensions both have spacing 1.0 in all three variants, so each boundary contact contributes 1.0×1.0 = 1.0. All three variants produce identical expected values. +**SSA note:** The 2.2 spacing is along the active axis. It does not contribute to face area. Both inactive spacings are 1, so each contact has area 1. -**SurfaceFeatures note — D2 regression:** Legacy DREAM3D 6.5.171 marks all features as `true` for any 1D image because `column==0` or `row==0` is always satisfied when that axis has size 1 (see `vv/deviations/ComputeFeatureNeighborsFilter.md`, D2). SIMPLNX correctly marks only features that touch the actual active-dimension boundary. +**SurfaceFeatures note — D2 regression:** DREAM.3D v6.5.171 marks all features as `true` for any 1D image because `column==0` or `row==0` is always satisfied when that axis has size 1 (see `vv/deviations/ComputeFeatureNeighborsFilter.md`, D2). SIMPLNX correctly marks only features that touch the actual active-dimension boundary. --- -### Fixture 2: 2D square (EmptyZ / EmptyY / EmptyX variants) +**Fixture 2: 2D square (EmptyZ / EmptyY / EmptyX variants)** **Source:** `Create2DEmptyZDataStructure()`, `Create2DEmptyYDataStructure()`, `Create2DEmptyXDataStructure()` -| Variant | Dimensions | Spacing (x,y,z) | X-normal face area | Y-normal face area | +| Variant | Dimensions | Spacing (x,y,z) | First active face area | Second active face area | |---|---|---|---|---| | EmptyZ | 5×5×1 | (2.2, 1.2, 1.0) | sy·sz = 1.2 | sx·sz = 2.2 | -| EmptyY | 5×1×5 | (2.2, 1.0, 1.2) | sy·sz = 1.2 | sx·sz = 2.2 | -| EmptyX | 1×5×5 | (1.0, 2.2, 1.2) | sy·sz = 2.64 (not used) | sx·sz = 1.2 | +| EmptyY | 5×1×5 | (2.2, 1.0, 1.2) | sy·sz = 1.2 | Z-normal: sx·sy = 2.2 | +| EmptyX | 1×5×5 | (1.0, 2.2, 1.2) | Y-normal: sx·sz = 1.2 | Z-normal: sx·sy = 2.2 | All three variants use the same feature layout mapped to the appropriate active plane; expected output values are identical by symmetry. @@ -116,15 +146,15 @@ y=4: [1, 2, 1, 0, 5] **SurfaceFeatures:** F3 and F4 are fully interior (no voxel touches any image edge) → false. F1, F2, F5 have boundary voxels → true. -**BoundaryCells:** Values range from 0 (interior same-feature voxels) to 4 (interior F2 voxels touching four different features). The full 25-element expected array is embedded at `ComputeFeatureNeighborsTest.cpp:220–245`. +**BoundaryCells:** Values range from 0 to 4. A count of 4 means that all four faces touch a different positive feature; those neighbor features need not be distinct from each other. `Fill2DImage()` contains all 25 expected values. --- -### Fixture 3: 2D non-square regression +**Fixture 3: 2D non-square regression** **Source:** `Create2DNonSquareEmpty{Z,Y,X}DataStructure()` -These fixtures specifically guard the row-stride bug fixed in PR #1590. A square 5×5 layout (dims[0]==dims[1]) masks a stride error in `initializeFaceNeighborOffsets` because either dimension produces the same offset. A non-square 3×2 layout exposes it: the wrong stride misses one boundary face, yielding SSA = 2×area instead of 3×area. +These fixtures distinguish the two active dimensions in the row stride. Square dimensions cannot detect an exchanged stride. Each non-square fixture has three shared faces with area 2, so the expected shared area is 6. | Variant | Dimensions | Spacing (x,y,z) | Boundary face normal | Face area | Expected SSA | |---|---|---|---|---|---| @@ -134,11 +164,9 @@ These fixtures specifically guard the row-stride bug fixed in PR #1590. A square **Layout (all three variants):** 6 total voxels; linear indices 0–2 = feature 1 (first "row"), indices 3–5 = feature 2 (second "row"). Both features touch the image boundary → SurfaceFeatures[1]=SurfaceFeatures[2]=true. Each voxel has exactly one cross-feature face contact → BoundaryCells=1 for all 6 voxels. F1 and F2 are mutual single neighbors: NumNeighbors=1, NeighborList={2} and {1}, SSA[1]={6.0}, SSA[2]={6.0}. -**Bug behavior (pre-PR #1590):** Wrong stride uses dims[1]=2 instead of dims[0]=3, stepping the wrong number of indices, finding only 2 of 3 boundary faces → SSA = 4.0 instead of 6.0. - --- -### Fixture 4: Full 3D +**Fixture 4: Full 3D** **Source:** `Create3DDataStructure()` @@ -156,7 +184,7 @@ These fixtures specifically guard the row-stride bug fixed in PR #1590. A square | 5 | true | 0 | {} | | 6 | false (fully interior) | 4 | {1, 2, 3, 4} | -Feature 5 is isolated (all face contacts are with other features or background except none — it simply has no shared-face neighbors among features 1–6). +Feature 5 touches only feature 5 and background, so its neighbor list is empty. **SSA (hand-derived; each value = Σ face_area over all shared boundary contacts for that pair):** @@ -173,23 +201,16 @@ Feature 5 is isolated (all face contacts are with other features or background e | F3–F6 | 15.36 | | F4–F6 | 16.20 | -Values are cross-checked for dimensional consistency: each is expressible as a×3.96 + b×2.16 + c×2.64 with non-negative integers a, b, c (e.g., F1–F2 = 20×3.96 + 3×2.16 + 5×2.64 = 98.88). The full FeatureIds 125-element layout and expected BoundaryCells 125-element array are embedded at `ComputeFeatureNeighborsTest.cpp:444–517`. +Each shared area is a sum of X-, Y- and Z-normal face areas. `Create3DDataStructure()` contains the 125 FeatureIds, all 125 expected boundary counts and the ten feature-pair area values above. -**Recommended for second-engineer review** — this is the highest-complexity hand-derivation in the suite. +## Oracle provenance (Classes 2, 3, 5 only) ---- +Not applicable. The correctness oracle is Class 1 and is encoded in the test source. -## Archive oracle role +## Second-engineer oracle review -| DataPath | Role | Notes | -|---|---|---| -| `DataContainer/CellFeatureData/SurfaceFeatures` | Legacy comparison reference | Bit-identical to SIMPLNX on SmallIn100 | -| `DataContainer/CellFeatureData/NumNeighbors` | Legacy comparison reference | Bit-identical to SIMPLNX on SmallIn100 | -| `DataContainer/CellFeatureData/NeighborList` | Legacy comparison reference | Bit-identical to SIMPLNX on SmallIn100 | -| `DataContainer/CellFeatureData/SharedSurfaceAreaList` | **NOT COMPARED** | Reflects the D1 SSA bug in DREAM3D 6.5.171; test explicitly skips this (`ComputeFeatureNeighborsTest.cpp:910`) | - ---- +The report sign-off names Nathan Young, 06-23-2026. It names no second engineer. -## Circular-oracle status +## Regenerated to fix a circular-oracle situation? -No circular-oracle risk for the inline fixtures — expected values are derived from geometry and FeatureIds alone, independent of any filter run. The archive's SSA arrays have a known D1 bug and are not used as oracle values. The 3D fixture's SSA table is the primary candidate for independent second-engineer re-derivation. +The shared archive changed in PR #1124 for the ComputeShapes voxel-center correction. Neighbor correctness uses independently specified C++ expectations. The archive's shared-area reference is omitted from comparison; the analytical tests check that output. From 8bc487b634f295d0afa05c85916ddf4e533e919a Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 20:01:28 -0400 Subject: [PATCH 44/91] DOC: Rewrite the ComputeFeaturePhases V&V record to follow the report template Signed-off-by: Michael Jackson --- .../vv/ComputeFeaturePhasesFilter.md | 176 +++++++++--------- .../deviations/ComputeFeaturePhasesFilter.md | 72 +++---- .../provenance/ComputeFeaturePhasesFilter.md | 92 ++++----- 3 files changed, 154 insertions(+), 186 deletions(-) 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/deviations/ComputeFeaturePhasesFilter.md b/src/Plugins/SimplnxCore/vv/deviations/ComputeFeaturePhasesFilter.md index 326cb05bab..1dddc6c7ea 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/ComputeFeaturePhasesFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/ComputeFeaturePhasesFilter.md @@ -1,81 +1,65 @@ -# Deviations from DREAM3D 6.5.171: ComputeFeaturePhasesFilter +# Deviations from DREAM.3D v6.5.171: ComputeFeaturePhasesFilter -This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent (`FindFeaturePhases`, SIMPL UUID `6334ce16-cea5-5643-83b5-9573805873fa`). +Part 1 lists the three differences from `FindFeaturePhases` in DREAM.3D v6.5.171. Entries retain the stable IDs `ComputeFeaturePhasesFilter-D1` through `ComputeFeaturePhasesFilter-D3`. The Filter UUID is the permanent cross-reference anchor. -Entries use stable IDs (`ComputeFeaturePhasesFilter-D`). D1, D2, and D3 are active. None affect `featurePhases[1..N]` for valid inputs. +## Part 1 — Differences from DREAM.3D v6.5.171 -> **Source references:** DREAM3D 6.5.171 `FindFeaturePhases::execute()` is in `DREAM3D/Source/Plugins/Generic/GenericFilters/FindFeaturePhases.cpp`. SIMPLNX algorithm is in `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeFeaturePhases.cpp`. - ---- - -## ComputeFeaturePhasesFilter-D1 +### ComputeFeaturePhasesFilter-D1 | Field | Value | |---|---| | **Deviation ID** | `ComputeFeaturePhasesFilter-D1` | -| **Filter UUID** | `da5bb20e-4a8e-49d9-9434-fbab7bc434fc` (SIMPL `6334ce16-cea5-5643-83b5-9573805873fa`) | +| **Filter UUID** | `da5bb20e-4a8e-49d9-9434-fbab7bc434fc` | | **Status** | active | -| **Tested by** | `SimplnxCore::ComputeFeaturePhasesFilter: Valid: Feature 0 Skip`, `SimplnxCore::ComputeFeaturePhasesFilter: Valid: Gap Feature Stays Zero` (zero-init contract) | -**Symptom:** `featurePhases[0]` is always `0` in SIMPLNX; under 6.5.171 it is written with the phase of the last cell whose `featureId == 0`. Confirmed empirically in the 2026-07-10 comparison run (Case C: SIMPL writes `featurePhases[0] = 2`; SIMPLNX leaves it `0`). +**Symptom:** SIMPLNX writes zero to feature zero. DREAM.3D v6.5.171 writes the phase of the last background cell. -**Root cause:** *algorithmic choice (explicit background skip).* The SIMPLNX loop contains `if(featureId == 0) { continue; }` before any write. The 6.5.171 `FindFeaturePhases::execute()` performs `m_FeaturePhases[gnum] = m_CellPhases[i]` unconditionally for every cell index `i` without any guard on `gnum == 0` (source: `FindFeaturePhases.cpp:202`), so background cells (featureId 0) are processed and `featurePhases[0]` receives the last background cell's phase. +**Root cause:** Algorithmic choice. `ComputeFeaturePhases.cpp` skips cells with FeatureId zero and writes a feature cache initialized to zero. `FindFeaturePhases::execute()` in DREAM.3D v6.5.171 assigns every cell phase, including background cells. This is a deliberate background convention, not a defect in the last-cell rule for positive features. -**Affected users:** Any consumer that reads `featurePhases[0]` directly. Standard downstream filters treat feature index 0 as the background and ignore it. +**Affected users:** Users who read feature zero. DREAM3D-NX v7.4.2 introduces background exclusion; DREAM3D-NX v7.0.0 through v7.4.1 process background cells as DREAM.3D v6.5.171 does. -**Recommendation:** Treat `featurePhases[0]` as undefined. Its value in SIMPLNX (0, from zero-initialization by `CreateArrayAction`) is not equivalent to the last background cell's phase produced by 6.5.171. +**Recommendation:** Trust SIMPLNX. Treat feature zero as background, not as a measured phase for a positive feature. + +**Evidence:** `Valid: Feature 0 Skip` checks output `[0,1,2]`. `Valid: Gap Feature Stays Zero` checks the separate unused-feature rule. Source comparison uses DREAM.3D tag `v6.5.171`, `Source/Plugins/Generic/GenericFilters/FindFeaturePhases.cpp`, and each SIMPLNX release source listed in `../provenance/ComputeFeaturePhasesFilter.md`. `vv_work:ComputeFeaturePhasesFilter/compare_outputs.py` specifies Case C expectations but is not a saved execution result. --- -## ComputeFeaturePhasesFilter-D2 +### ComputeFeaturePhasesFilter-D2 | Field | Value | |---|---| | **Deviation ID** | `ComputeFeaturePhasesFilter-D2` | -| **Filter UUID** | `da5bb20e-4a8e-49d9-9434-fbab7bc434fc` (SIMPL `6334ce16-cea5-5643-83b5-9573805873fa`) | +| **Filter UUID** | `da5bb20e-4a8e-49d9-9434-fbab7bc434fc` | | **Status** | active | -| **Tested by** | `SimplnxCore::ComputeFeaturePhasesFilter: Invalid: Negative Cell Phase`, `SimplnxCore::ComputeFeaturePhasesFilter: Valid: Negative Cell Phase On Background Cell` (guard scope) | +| **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:** Earlier versions could copy a negative phase into a positive feature without reporting an error. SIMPLNX now returns `-61861` for this input. -**Symptom:** A cell phases array containing a negative value in a valid feature (not 0) causes an immediate error (`-61861`) in SIMPLNX. Under 6.5.171 the same input is processed silently. +**Root cause:** Bug. Earlier implementations copied signed cell phases without checking for negative values. The current algorithm checks each phase after excluding background cells and before updating its feature cache. Storing a negative integer is defined arithmetic; the defect was accepting an invalid phase value. -**Root cause:** *algorithmic choice (defensive guard added).* SIMPLNX checks `if(currentPhaseId < 0) { return MakeErrorResult(-61861, ...); }` before each write. The 6.5.171 `FindFeaturePhases::execute()` has no corresponding check; `m_FeaturePhases[gnum] = m_CellPhases[i]` would silently store the negative value. +**Affected users:** Users with negative cell phases on positive features in DREAM3D-NX v7.0.0 through v7.4.1 or DREAM.3D v6.5.171. Negative phases on background cells are excluded from the current check. -**Affected users:** Pipelines that supply negative cell phase values. Such values represent invalid input; the legacy silent-accept behavior was a defect. SIMPLNX surfaces the error explicitly. +**Recommendation:** Trust SIMPLNX. Correct the negative phase before processing a positive feature; DREAM3D-NX v7.4.2 contains the validation fix. -**Recommendation:** Trust SIMPLNX. Negative phase values are invalid; the 6.5.171 silent-pass was undefined behavior. +**Evidence:** `Invalid: Negative Cell Phase` checks `-61861`; `Valid: Negative Cell Phase On Background Cell` checks success and output `[0,2]`. Direct source inspection finds no phase-sign guard in the nine release snapshots before v7.4.2 or in DREAM.3D tag `v6.5.171`; v7.4.2 contains the guard added by `bbc470e6d` (PR #1672). The release table in `../provenance/ComputeFeaturePhasesFilter.md` identifies the source paths. --- -## ComputeFeaturePhasesFilter-D3 +### ComputeFeaturePhasesFilter-D3 | Field | Value | |---|---| | **Deviation ID** | `ComputeFeaturePhasesFilter-D3` | -| **Filter UUID** | `da5bb20e-4a8e-49d9-9434-fbab7bc434fc` (SIMPL `6334ce16-cea5-5643-83b5-9573805873fa`) | +| **Filter UUID** | `da5bb20e-4a8e-49d9-9434-fbab7bc434fc` | | **Status** | active | -| **Tested by** | `SimplnxCore::ComputeFeaturePhasesFilter: Valid: Mixed Phase Warning`, `SimplnxCore::ComputeFeaturePhasesFilter: Valid: Truncated Mixed Feature Warning` | - -**Symptom:** When features have cells with inconsistent phases, the two implementations emit different warning message formats under the same warning code (`-500`). -- **SIMPLNX** emits a single line listing up to 15 affected feature IDs as a comma-separated list, followed by `"and N more occurrence(s)"` when more than 15 features are affected. Example: `"... Affected Phase Features: 2, 5, 7, and 3 more occurrences"`. -- **6.5.171** emits a multi-line message: a header line (`"Elements from some features did not all have the same phase Id. The last phase Id copied into each feature will be used"`) followed by one line per conflicting feature: `" Phase Feature X created Y warnings."` where `Y` is the number of phase-mismatched cells for that feature. All lines are joined with `\n` in a single `notifyWarningMessage` call. There is no truncation limit. - -**Root cause:** *message format rewritten at port time.* The legacy implementation tracked a per-feature conflict count in a `QMap warningMap` and emitted that count per feature. SIMPLNX replaces the count with a `std::vector warnFeature` (boolean per feature) and formats the output as a flat ID list rather than per-feature lines. SIMPLNX also caps the list at 15 IDs to keep the warning compact. - -**Affected users:** Scripts or tooling that parse warning message text. The warning code (`-500`) is unchanged; only the message text and structure differ. - -**Recommendation:** Trust SIMPLNX. The warning code is stable; do not parse the message body for programmatic use. - ---- +**Symptom:** Mixed phases produce different warning text under the same warning code, `-500`. -## Comparison runs +**Root cause:** Algorithmic choice. DREAM.3D v6.5.171 counts cells whose phase differs from the first phase of each feature. It combines a header and one line per affected feature in one warning. SIMPLNX collects conflicting feature IDs in a sorted set, lists up to 15 IDs and appends the omitted-feature count. Consecutive phase comparisons identify the same conflicting positive features as comparison with the first phase. Both implementations retain the last phase. -### 2026-07-10 — current (post-cleanup, 3 cases) +**Affected users:** Tools that parse warning text. DREAM3D-NX v7.0.0 through v7.2.2 emit a header warning and separate feature-count warnings. DREAM3D-NX v7.4.0 and v7.4.1 use one unlimited ID list; v7.4.2 introduces the 15-ID limit. -| Case | featureIds | cellPhases | Result | -|---|---|---|---| -| A | `[1,1,2,2,3,3,4,4]` | `[1,1,1,1,2,2,2,2]` | `featurePhases[1..4]` **BIT-IDENTICAL** | -| B | `[1,2,3,4,1,2,3,4]` | `[1,1,2,2,2,1,2,2]` | `featurePhases[1..4]` **BIT-IDENTICAL** | -| C | `[0,0,1,1,2,2,3,3]` | `[2,2,1,1,2,2,1,1]` | `featurePhases[1..3]` bit-identical; `featurePhases[0]`: SIMPL=2, SIMPLNX=0 — **D1 confirmed** | +**Recommendation:** Trust SIMPLNX. Use warning code `-500` to identify mixed phases; avoid parsing the message body as a stable interface. -**Conclusion:** `featurePhases[1..N]` is bit-identical across all three cases. The only output deviation is D1 at `featurePhases[0]` when background cells are present. D2 and D3 affect error/warning paths only. +**Evidence:** `Valid: Mixed Phase Warning` checks one warning, ID 1 and final phase 2. `Valid: Truncated Mixed Feature Warning` checks IDs 1 through 15 and the omitted count of one. DREAM.3D `FindFeaturePhases.cpp` at tag `v6.5.171` contains the per-feature conflict counts. The release-source table in `../provenance/ComputeFeaturePhasesFilter.md` records the warning changes. diff --git a/src/Plugins/SimplnxCore/vv/provenance/ComputeFeaturePhasesFilter.md b/src/Plugins/SimplnxCore/vv/provenance/ComputeFeaturePhasesFilter.md index 418e3992e2..383cfd9dbc 100644 --- a/src/Plugins/SimplnxCore/vv/provenance/ComputeFeaturePhasesFilter.md +++ b/src/Plugins/SimplnxCore/vv/provenance/ComputeFeaturePhasesFilter.md @@ -1,74 +1,60 @@ -# Exemplar Provenance: ComputeFeaturePhasesFilter +# Exemplar Archive Provenance: 6_6_stats_test_v2.tar.gz + +This sidecar records the archive retired for `ComputeFeaturePhasesFilter` and the inline fixtures that replace its output comparison. ## Archive identity | Field | Value | |---|---| -| **Archive** | None — oracle data is inlined in test source | -| **SHA512** | N/A | -| **Used by** | 10 inline TEST_CASEs in `test/ComputeFeaturePhasesFilterTest.cpp` | -| **Generated by** | N/A — fixtures hand-constructed in-test | +| **Archive** | `6_6_stats_test_v2.tar.gz` | +| **SHA512** | `e84999dec914d81efce4fc4237c49c9bf32e48381b1e79f58aa4df934f0d7606cd7a948f9a5e7b17a126a7944cc531b531cfdc70756ca3e2207b20734e089723` | +| **Used by tests** | No current `ComputeFeaturePhasesFilter` test; formerly `SimplnxCore::ComputeFeaturePhasesFilter(Valid Parameters)` | +| **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) | -No exemplar archive is required. The algorithm is a single-pass indirection (`featurePhases[featureIds[i]] = cellPhases[i]`) with integer arithmetic; expected outputs are trivially hand-computable. +The archive remains registered in `src/Plugins/SimplnxCore/test/CMakeLists.txt`. Current feature-size, feature-neighbor, distance-map and surface-area tests still use it. ---- +## How it was generated -## Circular-oracle disposition +PR #1124 first registered this archive while updating the Small IN100 test data for a voxel-center correction in ComputeShapes. The former feature-phase test loaded `6_6_stats_test_v2.dream3d` and compared computed phases with the stored `DataContainer/Cell Feature Data/Phases` array. PR #1672 (`bbc470e6d`) replaced that test with ten hand-built fixtures in `src/Plugins/SimplnxCore/test/ComputeFeaturePhasesFilterTest.cpp`. Those fixtures create their cell and feature arrays directly in C++. -The pre-V&V test `SimplnxCore::ComputeFeaturePhasesFilter(Valid Parameters)` loaded `6_6_stats_test_v2.dream3d` and compared the filter's output against `Cell Feature Data/Phases` already in that archive. That `Phases` array was generated by running `FindFeaturePhases` (the SIMPL predecessor) as part of the Small IN100 pipeline — making the comparison circular: filter output checked against its own prior output. +The comparison inputs and expectations are separate artifacts: `vv_work:ComputeFeaturePhasesFilter/generate_inputs.py`, `vv_work:ComputeFeaturePhasesFilter/compare_outputs.py`, and the six `pipeline_simpl_.json` and `pipeline_nx_.d3dpipeline` files. `vv_work:ComputeFeaturePhasesFilter/run_ab_test.sh` names a stock DREAM.3D v6.5.171 runner, but invokes only Cases A and B. The folder contains no ReadMe or result files. The reported comparison date is 2026-07-10; subsequent algorithm changes are `bbc470e6d` (2026-08-12, PR #1672: validation and background handling) and `4ec21e93f` (2026-09-20, PR #1568: chunked reads and cached output). -| Field | Value | -|---|---| -| **Archive** | `6_6_stats_test_v2.tar.gz` | -| **SHA512** | `e84999dec914d81efce4fc4237c49c9bf32e48381b1e79f58aa4df934f0d7606cd7a948f9a5e7b17a126a7944cc531b531cfdc70756ca3e2207b20734e089723` | -| **Circular array** | `DataContainer/Cell Feature Data/Phases` — not an independent oracle | -| **Retained for** | Nothing — fully retired. No active test references this archive. (`SIMPL Backwards Compatibility` loads `.json` SIMPL conversion fixtures, not `.dream3d` files.) | - -**Resolution (2026-07-10):** Retired `(Valid Parameters)` test. Replaced by 10 inline Class 1 oracle TEST_CASEs in `test/ComputeFeaturePhasesFilterTest.cpp` covering all code paths except cancel-signal injection. +## Canonical oracle output ---- +The current oracle is in the test source. No archived phase array supplies its expected values. For each positive feature, select the last cell in input order; its phase is the expected feature phase. Feature zero and unused features have value zero. -## Inline test fixtures +| DataPath | Source of expected values | +|---|---| +| `FeatureData/Phases` in `Valid: Uniform Phases` | Class 1: IDs `[1,1,2,2,2,3,3]` and phases `[1,1,2,2,2,1,1]` give `[0,1,2,1]`. | +| `FeatureData/Phases` in `Valid: Feature 0 Skip` | Class 1: IDs `[0,0,1,1,2,2]` and phases `[2,2,1,1,2,2]` give `[0,1,2]`. | +| `FeatureData/Phases[1]` in `Valid: Mixed Phase Warning` | Class 1: phases `[1,2]` on feature 1 give final phase 2 and one conflicting feature. | +| `FeatureData/Phases` in `Valid: Gap Feature Stays Zero` | Class 1: features 1 and 3 have phases 1 and 2; all five output values are `[0,1,0,2,0]`. | +| `FeatureData/Phases` in `Valid: Negative Cell Phase On Background Cell` | Class 1: IDs `[0,1,1]` and phases `[-5,2,2]` give `[0,2]`. | -All oracle assertions are embedded directly in `test/ComputeFeaturePhasesFilterTest.cpp`. No archived outputs are used. +The remaining fixtures check warning truncation and errors `-5351`, `-61860`, `-61861` and `-5355`. The truncation fixture checks the first 15 IDs and `and 1 more`; it does not compare output phases. Shape checks and absence of warnings on uniform positive features provide Class 4 checks. All pass in CI at `419f9db35` on Linux, macOS and Windows in-core builds. -**Derivation methodology:** `featurePhases[f]` equals the phase of the last cell (ascending index) belonging to feature `f`. For consistent datasets the result is unambiguous. `featurePhases[0]` is always `0` (zero-initialized by `CreateArrayAction`; never written — deviation D1). +Release-source inspection used every listed `upstream/release/v` snapshot. Source paths in the table are relative to `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/`. -| Case | TEST_CASE suffix | Purpose | Paths | -|---|---|---|---| -| 1 | `Valid: Uniform Phases` | 7 cells, 3 features, no conflict; normal accumulation, no warning | 6, 7, 10 | -| 2 | `Valid: Feature 0 Skip` | 6 cells, 2 features + background; `featurePhases[0]` stays 0 (D1) | 4, 10 | -| 3 | `Valid: Mixed Phase Warning` | 2 cells, 1 feature, phase conflict; warning code -500 | 8, 9 | -| 4 | `Valid: Truncated Mixed Feature Warning` | 16 conflicting features; truncation suffix "and 1 more occurrence" | 9 (truncation) | -| 5 | `Invalid: FeatureId Out Of Bounds` | featureId 5 with Feature AM of 3 tuples; error -5351 | 1 (partial — OOB only) | -| 6 | `Invalid: Cell Array Size Mismatch` | featureIds (4 tuples) vs cellPhases (5 tuples); preflight error -61860 | 2 | -| 7 | `Invalid: Negative Cell Phase` | cellPhases contains -1; error -61861 | 5 | -| 8 | `Invalid: Negative FeatureId` | featureIds contains -1; error -5355 | 1b | -| 9 | `Valid: Gap Feature Stays Zero` | 4 cells over features 1 and 3 with a 5-tuple Feature AM; features 2 and 4 have no cells and stay 0 | 11 | -| 10 | `Valid: Negative Cell Phase On Background Cell` | cellPhases[0] = -5 on a `featureId == 0` cell; skipped before the -61861 guard, no error | 4, 5b | +| Released versions | Source | Background cells | Negative phases on positive features | Mixed-phase warning | +|---|---|---|---|---| +| DREAM3D-NX v7.0.0, v7.0.1, v7.0.2, v7.0.3, v7.2.0, v7.2.1, v7.2.2 | `ComputeFeaturePhasesFilter.cpp`, `executeImpl()` | Processed | Accepted | Header and per-feature count warnings | +| DREAM3D-NX v7.4.0 | `ComputeFeaturePhasesFilter.cpp`, `executeImpl()` | Processed | Accepted | One unlimited ID list | +| DREAM3D-NX v7.4.1 | `Algorithms/ComputeFeaturePhases.cpp` | Processed | Accepted | One unlimited ID list | +| DREAM3D-NX v7.4.2 | `Algorithms/ComputeFeaturePhases.cpp` | Skipped | Rejected with `-61861` | One list limited to 15 IDs | ---- +DREAM.3D tag `v6.5.171`, `Source/Plugins/Generic/GenericFilters/FindFeaturePhases.cpp`, processes background cells and accepts negative phases. It combines the per-feature count lines in one warning. D2 was therefore present in DREAM.3D v6.5.171 and DREAM3D-NX v7.0.0 through v7.4.1; DREAM3D-NX v7.4.2 contains its fix. -## Canonical oracle output +## Oracle provenance (Classes 2, 3, 5 only) -| Assertion | Class | Derivation | -|---|---|---| -| `featurePhases[1..3]` in Case 1 = `[1, 2, 1]` | 1 — Analytical | Last cell of each feature has unambiguous consistent phase | -| `featurePhases[1]` in Case 3 = 2 | 1 — Analytical | Last-cell-wins: cell 1 (phase 2) overwrites cell 0 (phase 1) | -| `featurePhases[0]` in all cases = 0 | 1 — Analytical | Zero-initialized; never written (deviation D1) | -| `featurePhases[2]` and `featurePhases[4]` in Case 9 = 0 | 1 — Analytical | No cell carries those feature ids, so the loop never writes them; `CreateArrayAction` zero-initializes | -| No error for Case 10 | 1 — Analytical | The `featureId == 0` skip precedes the `currentPhaseId < 0` guard, so the background cell's -5 is never read | -| Warning count = 0 for Cases 1, 2 | 4 — Invariant | No feature has mixed-phase cells | -| Warning code = -500 for Cases 3, 4 | 1 — Analytical | Hard-coded in algorithm | -| Error code = -5351 for Case 5 | 1 — Analytical | Hard-coded in `ValidateFeatureIdsToFeatureAttributeMatrixIndexing` | -| Error code = -5355 for Case 8 | 1 — Analytical | Hard-coded in `ValidateFeatureIdsToFeatureAttributeMatrixIndexing` | -| Preflight error code = -61860 for Case 6 | 1 — Analytical | Hard-coded in `preflightImpl()` (`ComputeFeaturePhasesFilter.cpp:102`) | -| Error code = -61861 for Case 7 | 1 — Analytical | Hard-coded in algorithm | -| `featurePhases[f] ∈ {phases of cells with featureId == f}` for `f > 0` | 4 — Invariant | Single-pass indirection | - ---- +Not applicable. Classes 1 and 4 use literal expectations and predicates in the test source. ## Second-engineer oracle review -- **Status:** Not yet conducted. -- **Rationale for skipping if necessary:** Cases 1–4, 9, and 10 are single-pass indirections over ≤ 32 cells with small integer phase values; Cases 5–8 are hard-coded error codes. Derivation is reproducible from the algorithm source in under 5 minutes. +- **Reviewer:** Not recorded. +- **Date:** Not recorded. + +## Regenerated to fix a circular-oracle situation? + +No archive was regenerated for this filter's replacement tests. PR #1672 removed the stored-output comparison and introduced independent inline fixtures. The archive is retired only for this filter; other SimplnxCore tests still consume it. From c90bab821273790e241793cda9bd018a04094822 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 20:01:28 -0400 Subject: [PATCH 45/91] DOC: Rewrite the ComputeFeatureSizes V&V record to follow the report template Signed-off-by: Michael Jackson --- .../vv/ComputeFeatureSizesFilter.md | 36 +++++++++---------- .../deviations/ComputeFeatureSizesFilter.md | 30 ++++++++-------- .../provenance/ComputeFeatureSizesFilter.md | 13 ++++--- 3 files changed, 40 insertions(+), 39 deletions(-) diff --git a/src/Plugins/SimplnxCore/vv/ComputeFeatureSizesFilter.md b/src/Plugins/SimplnxCore/vv/ComputeFeatureSizesFilter.md index 8e66381b01..e3e52205fe 100644 --- a/src/Plugins/SimplnxCore/vv/ComputeFeatureSizesFilter.md +++ b/src/Plugins/SimplnxCore/vv/ComputeFeatureSizesFilter.md @@ -15,13 +15,13 @@ | 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 registered tests pass in CI at `01809c236`; the flat-spacing case is an expected failure (`ComputeFeatureSizes-D3`). | -| Code paths enumerated | 24 of 44 paths have test assertions. Gaps include input read errors, count overflow, and several cancellation paths. | +| 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 | 24 of 44 paths exercised with test assertions. Gaps include input read errors, count overflow, and several cancellation paths. | | Tests today | 16 test cases: 14 visible and 2 hidden OOC cases. Coverage includes analytical values, output writes, invalid inputs, 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 | Source comparison with DREAM.3D v6.5.171 confirms D1–D3. Runtime comparison artifacts are not available. | -| Bug flags | `ComputeFeatureSizes-D3` (issue #1857) | -| V&V phase | Complete. `ComputeFeatureSizes-D3` remains open; its regression test is marked as an expected failure. | +| 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 @@ -39,22 +39,22 @@ *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 registered tests pass in CI at `01809c236`. The flat-spacing case retains `[!shouldfail]` for `ComputeFeatureSizes-D3`. +*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:* Michael Jackson, 2026-07-28 (recorded sign-off). +*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. ## Bugs found and fixed -PR #1638 fixed `ComputeFeatureSizes-D4` and `ComputeFeatureSizes-D5`, and DREAM3D-NX v7.4.2 contains those fixes. +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`. -| Deviation | Defect | Affected released versions | Resolution in this branch | -|-----------|--------|----------------------------|---------------------------| -| `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. | Subtract the compensation from the incoming partial sum before adding it to the accumulated total. | +| 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 -24 of 44 paths have test assertions. Gaps include input read errors, count overflow, and cancellation during input processing. +24 of 44 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` (199 lines). @@ -118,13 +118,13 @@ All names are in `src/Plugins/SimplnxCore/test/ComputeFeatureSizesTest.cpp`. Dyn | `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. PR #1638 changed float comparisons to relative tolerance `1e-6` for platform and reduction-order variation. | +| `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. PR #1638 changed float comparisons to 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. PR #1638 changed float comparisons to 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. PR #1638 changed float comparisons to 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. PR #1638 changed float comparisons to 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. PR #1638 changed float comparisons to relative tolerance `1e-6` for platform and reduction-order variation. | +| `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. | diff --git a/src/Plugins/SimplnxCore/vv/deviations/ComputeFeatureSizesFilter.md b/src/Plugins/SimplnxCore/vv/deviations/ComputeFeatureSizesFilter.md index c1ba311647..c8f76bdeda 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/ComputeFeatureSizesFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/ComputeFeatureSizesFilter.md @@ -55,18 +55,16 @@ Entries use the stable ID sequence `ComputeFeatureSizes-D`. The Filter UUID i | **Deviation ID** | `ComputeFeatureSizes-D3` | | **Filter UUID** | `c666ee17-ca58-4969-80d0-819986c72485` | | **Status** | active | -| **Affected released versions** | DREAM3D-NX v7.4.2. DREAM.3D v6.5.171 was not affected. | -| **Fixed in** | Not fixed. Tracked in issue #1857. | **Symptom:** For a 2D image, the reported area includes the flat dimension's spacing, and the circular diameter uses that value. -**Root cause:** Bug in the area and diameter interpretation. PR #1590 (`e76ad508e`) replaced the two non-flat spacings with the product of all three spacings. Both current algorithms keep this slab convention and then apply `2*sqrt(value/pi)`. With physical length units, the product is a volume, so it is not an area for the circle formula. DREAM.3D v6.5.171 uses only the two non-flat spacings. +**Root cause:** Bug in the area and diameter interpretation. DREAM3D-NX v7.4.2 uses the product of all three spacings instead of the two non-flat spacings. Both current algorithms keep this slab convention and then apply `2*sqrt(value/pi)`. With physical length units, the product is a volume, so it is not an area for the circle formula. DREAM.3D v6.5.171 uses only the two non-flat spacings. -**Affected users:** Users who need true 2D area and circular diameter, especially when flat spacing is not 1. For four cells with non-flat spacings 2 and 3 and flat spacing 5, the algorithms calculate 120; the true area is 24. Relative to true area, the size scales by flat spacing and the diameter scales by its square root. +**Affected users:** Users of DREAM3D-NX v7.4.2 who need true 2D area and circular diameter, especially when flat spacing is not 1. DREAM.3D v6.5.171 was not affected. For four cells with non-flat spacings 2 and 3 and flat spacing 5, the algorithms calculate 120; the true area is 24. Relative to true area, the size scales by flat spacing and the diameter scales by its square root. -**Recommendation:** Do not interpret these outputs as true area and circular diameter when flat spacing is not 1. For true 2D measurements, use the product of the two non-flat spacings for area and `2*sqrt(area/pi)` for diameter. +**Recommendation:** This defect is not fixed. Do not interpret these outputs as true area and circular diameter when flat spacing is not 1. For true 2D measurements, use the product of the two non-flat spacings for area and `2*sqrt(area/pi)` for diameter. -**Evidence:** `SimplnxCore::ComputeFeatureSizes: 2D area excludes the flat-dimension spacing` asserts 24 for flat X, Y, and Z and has `[!shouldfail]`. The Scanline `bounded output writes` Image 2D variant instead asserts the current slab result. Source in `upstream/release/v7.0.0` through `upstream/release/v7.4.1` excludes the flat spacing. Source in `upstream/release/v7.4.2` includes all three spacings. +**Evidence:** PR #1590 (`e76ad508e`) introduced this defect, which is tracked in issue #1857. `SimplnxCore::ComputeFeatureSizes: 2D area excludes the flat-dimension spacing` asserts 24 for flat X, Y, and Z and is an expected failure (`[!shouldfail]`). The Scanline `bounded output writes` Image 2D variant instead asserts the current slab result. Source in `upstream/release/v7.0.0` through `upstream/release/v7.4.1` excludes the flat spacing. Source in `upstream/release/v7.4.2` includes all three spacings. --- @@ -80,15 +78,17 @@ Entries use the stable ID sequence `ComputeFeatureSizes-D`. The Filter UUID i | **Filter UUID** | `c666ee17-ca58-4969-80d0-819986c72485` | | **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** | PR #1638 (`8d0b0ed22`), DREAM3D-NX v7.4.2. | +| **Fixed in** | DREAM3D-NX v7.4.2 | **Symptom:** RectGrid feature-volume reduction could lose the intended compensation for floating-point rounding. -**Root cause:** Bug. PR #1592 (`aaad2c4e2`) added the parallel reduction. It subtracted the compensation from the accumulated total, added that value to the incoming partial sum, and measured the rounding residual relative to the incoming sum. PR #1638 applies the usual Kahan update: subtract the compensation from the incoming partial sum, add it to the total, and calculate the residual relative to the total. The corrected code is in `ComputeFeatureSizesDirect.cpp`. +**Root cause:** Bug. The parallel reduction subtracted the compensation from the accumulated total, added that value to the incoming partial sum, and measured the rounding residual relative to the incoming sum. The filter now applies the usual Kahan update: subtract the compensation from the incoming partial sum, add it to the total, and calculate the residual relative to the total. The corrected code is in `ComputeFeatureSizesDirect.cpp`. -**Affected users:** Users of development revisions between PR #1592 and PR #1638 who calculate RectGrid feature volumes. DREAM3D-NX v7.0.0 through v7.4.2 were not affected. +**Affected users:** Users of development versions that used the incorrect parallel reduction to calculate RectGrid feature volumes. DREAM3D-NX v7.0.0 through v7.4.2 were not affected. -**Recommendation:** Use a revision that contains PR #1638. The RectGrid analytical tests check feature volumes and diameters, but do not isolate the compensation correction with an adversarial rounding fixture. +**Recommendation:** Trust SIMPLNX in DREAM3D-NX v7.4.2. The filter applies compensation to each incoming partial sum. + +**Evidence:** PR #1592 (`aaad2c4e2`) introduced the defect. PR #1638 (`8d0b0ed22`) fixed it before DREAM3D-NX v7.4.2. The release-source inspection is recorded in `../provenance/ComputeFeatureSizesFilter.md`. The RectGrid analytical tests check feature volumes and diameters, but do not isolate this correction with a fixture that exposes the rounding error. --- @@ -100,12 +100,14 @@ Entries use the stable ID sequence `ComputeFeatureSizes-D`. The Filter UUID i | **Filter UUID** | `c666ee17-ca58-4969-80d0-819986c72485` | | **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** | PR #1638 (`8d0b0ed22`), DREAM3D-NX v7.4.2. | +| **Fixed in** | DREAM3D-NX v7.4.2 | **Symptom:** Parallel Image and RectGrid workers could read nonresident FeatureIds without first disabling parallel scheduling. -**Root cause:** Bug. PR #1592 introduced parallel reads without a residency guard. PR #1638 added `requireStoresInMemory()` for FeatureIds before each `ParallelDataAlgorithm::execute()` call. The current Direct implementation retains both calls. The guard controls scheduling; it does not pin data or synchronize store access. The RectGrid guard checks FeatureIds only, not element sizes. +**Root cause:** Bug. Parallel reads had no residency guard. The filter now calls `requireStoresInMemory()` for FeatureIds before each `ParallelDataAlgorithm::execute()` call. The current Direct implementation retains both calls. The guard controls scheduling; it does not pin data or synchronize store access. The RectGrid guard checks FeatureIds only, not element sizes. + +**Affected users:** Users of development versions that scheduled parallel reads of nonresident FeatureIds without a residency guard. DREAM3D-NX v7.0.0 through v7.4.2 were not affected. -**Affected users:** Users of development revisions between PR #1592 and PR #1638 with nonresident FeatureIds and parallel execution. DREAM3D-NX v7.0.0 through v7.4.2 were not affected. +**Recommendation:** Trust SIMPLNX in DREAM3D-NX v7.4.2. Direct execution uses serial scheduling when FeatureIds are nonresident. -**Recommendation:** Use a revision that contains PR #1638. Current real-store tests require Scanline dispatch and do not directly exercise this Direct scheduling fallback. +**Evidence:** PR #1592 (`aaad2c4e2`) introduced the defect. PR #1638 (`8d0b0ed22`) added both residency guards before DREAM3D-NX v7.4.2. The release-source inspection is recorded in `../provenance/ComputeFeatureSizesFilter.md`. Current real-store tests require Scanline dispatch and do not directly exercise this Direct scheduling fallback. diff --git a/src/Plugins/SimplnxCore/vv/provenance/ComputeFeatureSizesFilter.md b/src/Plugins/SimplnxCore/vv/provenance/ComputeFeatureSizesFilter.md index 246f69dd99..baec632902 100644 --- a/src/Plugins/SimplnxCore/vv/provenance/ComputeFeatureSizesFilter.md +++ b/src/Plugins/SimplnxCore/vv/provenance/ComputeFeatureSizesFilter.md @@ -36,7 +36,7 @@ The three builders create synthetic geometries and hand-specified FeatureIds on 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. -Relevant Information for the data set: +Input values: ```text Image Info: @@ -71,7 +71,7 @@ Feature Ids: 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. -Relevant Information for the data set: +Input values: ```text Image Info: @@ -91,7 +91,7 @@ Feature Ids: 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. -Relevant Information for the data set: +Input values: ```text Image Info: @@ -141,7 +141,7 @@ The Class 1 oracle lives in the test code. The following paths refer to the inli | `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 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 retains `[!shouldfail]`: it expects area 24 for flat X, Y, and Z, while the filter calculates 120 (`ComputeFeatureSizes-D3`). +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** @@ -348,7 +348,7 @@ Each fetched release branch was inspected with `git show upstream/release/v Date: Wed, 21 Oct 2026 20:01:28 -0400 Subject: [PATCH 46/91] DOC: Rewrite the ComputeGroupingDensity V&V record to follow the report template Signed-off-by: Michael Jackson --- .../vv/ComputeGroupingDensityFilter.md | 206 +++++++----------- .../ComputeGroupingDensityFilter.md | 92 +------- .../ComputeGroupingDensityFilter.md | 150 ++++++------- 3 files changed, 152 insertions(+), 296 deletions(-) diff --git a/src/Plugins/SimplnxCore/vv/ComputeGroupingDensityFilter.md b/src/Plugins/SimplnxCore/vv/ComputeGroupingDensityFilter.md index 8d097d0982..58bfe2ffae 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 against DREAM.3D v6.5.171: it has no equivalent filter, and the comparison working folder is absent. The archive records a local proof comparison signed off on 2026-05-27; the algorithm source 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/deviations/ComputeGroupingDensityFilter.md b/src/Plugins/SimplnxCore/vv/deviations/ComputeGroupingDensityFilter.md index 5ec1b90fc9..faa550d970 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/ComputeGroupingDensityFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/ComputeGroupingDensityFilter.md @@ -1,91 +1,5 @@ -# Deviations from Pre-SIMPLNX Implementation: ComputeGroupingDensityFilter +# Deviations from DREAM.3D v6.5.171: ComputeGroupingDensityFilter -> **Note (this filter only):** the title has been re-scoped from the template -> default "Deviations from DREAM3D 6.5.171." `ComputeGroupingDensityFilter` is -> a port of the pre-SIMPLNX `FindGroupingDensity` (SIMPL UUID -> `708be082-8b08-4db2-94be-52781ed4d53d`) on `tuks188/DREAM3D` -> `feature/770_Grouping_Density` — not the shipped 6.5.171 baseline. -> The legacy filter was never officially released, but a small set of customers -> consumed a custom DREAM3D 6.5.x build that included it. See the V&V report's -> Algorithm Relationship section for context. +DREAM.3D v6.5.171 has no equivalent filter. `FindGroupingDensity` in a local proof version supplied the port source. -## Headline - -**No deviations observed.** All 4 `(UseNonContiguousNeighbors, FindCheckedFeatures)` -configurations 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` arrays when run against identical -input data. - -## Comparison method - -| | | -|---|---| -| **Comparison type** | Runtime A/B (not analytical — both implementations actually executed) | -| **Identical inputs** | Same legacy-format `.dream3d` file consumed by both sides (no per-implementation data prep) | -| **Tolerance** | Bit-identical (`np.array_equal` on raw float32 / int32 bytes — stricter than any float epsilon) | -| **Configurations exercised** | All 4 — `(NC, CF) ∈ {(0,0), (0,1), (1,0), (1,1)}` covering every template specialization of `FindDensityGrouping()` | -| **Comparison driver** | `src/Plugins/SimplnxCore/vv/comparisons/ComputeGroupingDensityFilter/compare_outputs.py` | -| **Comparison fixture archive** | `compute_grouping_densities_v2.tar.gz` | -| **Archive SHA512** | `3aaabb63c4fa16f7fa192ae4ee9dbba9394ec7f1cd19aff55e399a624d495a3a778c7f6f282911f681e85cea99e4c6d15344274e9107f337af7d4a19f93784ff` | - -### Inputs - -Hand-built minimal dataset matching `createTestDataStructure()` in -`test/ComputeGroupingDensityTest.cpp` (lines 62-141): - -| Path | Values | -|---|---| -| `DataContainer/FeatureData/Volumes` (Float32, 6) | `[0, 10, 20, 15, 25, 30]` | -| `DataContainer/FeatureData/ParentIds` (Int32, 6) | `[0, 1, 1, 1, 2, 2]` | -| `DataContainer/FeatureData/ContiguousNeighborList` | `[[], [2], [1,3], [2,4], [3,5], [4]]` | -| `DataContainer/FeatureData/NonContiguousNeighborList` | `[[], [4], [5], [], [1], [2]]` | -| `DataContainer/ParentData/ParentVolumes` (Float32, 3) | `[0, 45, 55]` | - -### Per-configuration result - -| (NC, CF) | `GroupingDensities` (both sides, float32) | `CheckedFeatures` (both sides, int32) | Diff | -|---|---|---|---| -| (0, 0) | `[0.0, 0.6428571343421936, 0.7857142686843872]` | array not produced (CF=false) | bit-identical | -| (0, 1) | `[0.0, 0.6428571343421936, 0.7857142686843872]` | `[0, 1, 1, 2, 2, 2]` | bit-identical | -| (1, 0) | `[0.0, 0.44999998807907104, 0.550000011920929]` | array not produced (CF=false) | bit-identical | -| (1, 1) | `[0.0, 0.44999998807907104, 0.550000011920929]` | `[0, 2, 2, 2, 2, 2]` | bit-identical | - -The numerical values match the SIMPLNX unit-test hand calculations exactly -(45/70, 55/70 for the contiguous-only cases; 45/100, 55/100 for the -non-contiguous-included cases — these are the exact float32 representations). - -### Build/source provenance - -| Side | Build / source | -|---|---| -| Legacy (local rebuild) | Local build of the legacy DREAM3D 6.5 source (`tuks188/DREAM3D` `feature/770_Grouping_Density` sources pulled in). `FindGroupingDensity.{cpp,h}` placed at `Source/Plugins/Statistics/StatisticsFilters/`. | -| SIMPLNX | `Workspace3/DREAM3D-Build/NX-Com-Qt69-Vtk95-Rel/Bin/nxrunner` (1.7.0 build 2026/05/07). Filter at `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeGroupingDensity.{hpp,cpp}` and `.../Filters/ComputeGroupingDensityFilter.{hpp,cpp}`. | - -## Algorithmic deltas observed (none affect output) - -For audit completeness, the SIMPLNX port made the following structural changes -versus the legacy `execute()` body. The runtime A/B above confirms each is -output-preserving: - -1. **Container swap:** `QVector` totalCheckList (linear `.contains()`, O(n²) per parent) - → `std::unordered_set` (O(1) membership). **No output change** — both have set-membership semantics on the same set of feature ids; floating-point accumulation order is unaffected. -2. **Boolean dispatch:** runtime `if (m_FindCheckedFeatures == true)` inside the inner loop - → compile-time `if constexpr (FindingCheckedFeatures)` template specialization. **No output change.** -3. **Neighbor-list unroll:** runtime `for(k=0; k checkedfeaturevolumes(numfeatures, 0.0f)` always allocated - → conditionally allocated only when `FindCheckedFeatures==true`. **No output change** (legacy zeros were never read when the flag was false). -5. **Cancellation support:** added `m_ShouldCancel` check in the outer parent loop. (Legacy had no cancel support; A/B was run without cancel so this code path is not exercised by the comparison.) -6. **Progress reporting:** added throttled per-parent progress feedback. (Legacy emits one terminal "Complete" message; SIMPLNX emits per-parent updates. Affects logs only.) - -Both implementations also behave the same way for the `FindCheckedFeatures=false` -cases — **neither** writes the `CheckedFeatures` output array (it's omitted, not -written-and-empty). - -## Migration recommendation for customers of the legacy custom build - -**Trust SIMPLNX. Output is bit-identical to the legacy filter for matched -inputs across all 4 configurations.** No migration tolerance band is required. -Downstream consumers can expect numerically identical `GroupingDensities` and -`CheckedFeatures` arrays. +There are no Part 1 entries and no Part 2 entries. The V&V found no confirmed filter defect. The archive comparison and analytical expectations are documented in `../provenance/ComputeGroupingDensityFilter.md`. diff --git a/src/Plugins/SimplnxCore/vv/provenance/ComputeGroupingDensityFilter.md b/src/Plugins/SimplnxCore/vv/provenance/ComputeGroupingDensityFilter.md index 5aa1531f9e..4f77bb66f4 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 (inferred from PR #1631) | +| **Generated on** | — | +| **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. From f46c9da54eade696c339952235dd88a8dc577394 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 20:01:28 -0400 Subject: [PATCH 47/91] DOC: Rewrite the ComputeNeighborhoods V&V record to follow the report template Signed-off-by: Michael Jackson --- .../vv/ComputeNeighborhoodsFilter.md | 165 +++++++++--------- .../deviations/ComputeNeighborhoodsFilter.md | 95 +++++++--- 2 files changed, 147 insertions(+), 113 deletions(-) diff --git a/src/Plugins/SimplnxCore/vv/ComputeNeighborhoodsFilter.md b/src/Plugins/SimplnxCore/vv/ComputeNeighborhoodsFilter.md index 0d1bbc2d56..dd72712f10 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 | Not run — comparison artifacts are not available. | +| Bug flags | None | +| V&V phase | Verification records are complete. The sign-off has no named second engineer. | ## 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/deviations/ComputeNeighborhoodsFilter.md b/src/Plugins/SimplnxCore/vv/deviations/ComputeNeighborhoodsFilter.md index ef5ac8defe..9127831182 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/ComputeNeighborhoodsFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/ComputeNeighborhoodsFilter.md @@ -1,70 +1,111 @@ -# Deviations from DREAM3D 6.5.171: ComputeNeighborhoodsFilter +# Deviations from DREAM.3D v6.5.171: ComputeNeighborhoodsFilter -This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent (`FindNeighborhoods`). +Part 1 lists differences from `FindNeighborhoods` in DREAM.3D v6.5.171 (D1–D3). Part 2 lists fixed SIMPLNX defects (D4–D5). All entries use one stable ID sequence and the filter UUID as the permanent cross-reference anchor. -Entries are referenced by stable ID (`ComputeNeighborhoodsFilter-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 + +### ComputeNeighborhoodsFilter-D1 + +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeNeighborhoodsFilter-D1` | +| **Filter UUID** | `924c10e3-2f39-4c08-9d7a-7fe029f74f6d` | +| **Status** | active | + +**Symptom:** The same centroids, diameters, and multiplier can give different neighbor counts and lists in SIMPLNX and DREAM.3D v6.5.171. + +**Root cause:** Algorithmic choice. DREAM.3D v6.5.171 converts centroids to integer bins using the average diameter. It requires each absolute bin difference to be strictly less than `equivalentDiameter[i] * multiplier / averageDiameter`. SIMPLNX compares squared Euclidean distance with `(equivalentDiameter[i] * multiplier)^2` and includes equality. The average diameter controls the SIMPLNX search grid, not the distance limit. SIMPLNX also offers an absolute-radius mode without Equivalent Diameters; DREAM.3D v6.5.171 has no such mode. + +**Affected users:** Users who migrate `FindNeighborhoods` pipelines and compare neighbor counts or lists. Bin quantization and the different inclusion rules prevent a fixed count ratio between implementations. -Comparison fixture: `6_6_stats_test_v2.dream3d` (Small IN100, 620 features), `MultiplesOfAverage = 1.0`, run through DREAM3D 6.5.171 `PipelineRunner` and DREAM3D-NX `nxrunner`. +**Recommendation:** Trust SIMPLNX for centroid-distance neighborhoods. The sphere implements the stated search radius, and both radius modes have independent analytical fixtures. The UUID mapping preserves pipeline conversion but does not promise identical output from the different neighborhood definitions. + +**Evidence:** `Source/Plugins/Statistics/StatisticsFilters/FindNeighborhoods.cpp` at the DREAM.3D `v6.5.171` tag contains the strict bin comparisons. Current `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeNeighborhoods.cpp` contains the inclusive distance comparison. `SimplnxCore::ComputeNeighborhoods_SyntheticOracle` checks the exact-radius boundary, and `SimplnxCore::ComputeNeighborhoods_MultiplesAnalyticalOracle` checks per-feature asymmetry in `src/Plugins/SimplnxCore/test/ComputeNeighborhoodsTest.cpp`. --- -## ComputeNeighborhoodsFilter-D1 +### ComputeNeighborhoodsFilter-D2 | Field | Value | |---|---| -| **Deviation ID** | `ComputeNeighborhoodsFilter-D1` | +| **Deviation ID** | `ComputeNeighborhoodsFilter-D2` | | **Filter UUID** | `924c10e3-2f39-4c08-9d7a-7fe029f74f6d` | | **Status** | active | -**Symptom:** For the same input and `MultiplesOfAverage` value, SIMPLNX reports fewer neighbors per feature than 6.5.171 — on Small IN100 (mult=1) the mean is 5.55 (SIMPLNX) vs 10.93 (6.5.171), i.e. ~52%, with a per-feature correlation of 0.894. +**Symptom:** SIMPLNX has no Feature Phases input; DREAM.3D v6.5.171 requires that array. -**Root cause:** *Algorithmic choice.* Both implementations scale each feature's search reach by that feature's own Equivalent Sphere Diameter times the multiplier. They differ in the **shape** of the neighborhood region: +**Root cause:** Algorithmic choice. DREAM.3D v6.5.171 validates Feature Phases in `FindNeighborhoods::dataCheck()` but does not read its values in the neighborhood calculation. SIMPLNX removes the parameter and its conversion mapping. Removing this unused input does not change neighborhood values. -- **6.5.171** compares the per-axis integer bin difference against a per-feature `criticalDistance` (`|Δbinₓ| < cd && |Δbin_y| < cd && |Δbin_z| < cd`), which tests membership in an **axis-aligned box** in a space normalized by the average diameter. This over-counts: it accepts features in the box corners that lie beyond the nominal reach. -- **SIMPLNX** computes the true **Euclidean distance** between centroids and compares it against the radius (`distSq ≤ radius²`), i.e. a **sphere**. +**Affected users:** Users who convert a DREAM.3D v6.5.171 pipeline. The `FeaturePhasesArrayPath` setting is no longer used. -A sphere of radius `r` has ~0.52× the volume of a cube of half-width `r`, which quantitatively explains the ~52% ratio observed. The SIMPLNX result is the geometrically correct interpretation of "features within a search radius." +**Recommendation:** Trust SIMPLNX. Select Centroids and, for multiples mode, Equivalent Diameters; no phase array is required. -The comparison operator also differs: 6.5.171 used a **strict** `<` on bin differences, while SIMPLNX uses an **inclusive** `distSq ≤ radius²`, so a centroid lying exactly on the sphere surface is counted as a neighbor. This boundary is measure-zero for real data but is an intentional, pinned choice (see the boundary-inclusion assertion in `test/ComputeNeighborhoodsTest.cpp`). +**Evidence:** The tagged legacy `FindNeighborhoods.cpp` requires Feature Phases only during input validation. Current `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeNeighborhoodsFilter.cpp` has no phase parameter or `FromSIMPLJson()` mapping. The two analytical fixtures create no phase array, and the SIMPL conversion test checks the remaining argument values. -**Affected users:** Anyone migrating a `FindNeighborhoods` pipeline from 6.5.171 and comparing `Neighborhoods` / `NeighborhoodList` values. Absolute counts will be lower under SIMPLNX; relative structure is preserved (corr 0.894). +--- -**Recommendation:** *Trust SIMPLNX.* The Euclidean sphere is the correct region for a centroid-distance neighborhood; the 6.5.171 box test was a bin-scan approximation. Users needing an exact reproduction of a specific physical search size should use the new "Search Radius (microns)" mode with an explicit radius. +### ComputeNeighborhoodsFilter-D3 -> **Note (SIMPLNX-internal regression, fixed in the V&V PR — not a 6.5.171 deviation):** Between the original NX port and this V&V, PR #1485 correctly switched to a Euclidean distance test but also introduced a `÷2` factor and a single **global** radius (`avgDiameter·mult÷2`) in place of the per-feature radius. At the default `mult=1` this found ~37× fewer neighbors than 6.5.171 (mean 0.29 vs 10.93). The V&V restored the per-feature radius `equivalentDiameter[i]·mult`; the numbers above are post-fix. +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeNeighborhoodsFilter-D3` | +| **Filter UUID** | `924c10e3-2f39-4c08-9d7a-7fe029f74f6d` | +| **Status** | active | + +**Symptom:** SIMPLNX rejects a non-positive multiplier with error `-5732`; DREAM.3D v6.5.171 accepts it. SIMPLNX also rejects a non-positive absolute radius with `-5733`. + +**Root cause:** Algorithmic choice. SIMPLNX validates the active radius parameter before creating outputs. DREAM.3D v6.5.171 has no multiplier positivity check. With positive feature diameters, its strict comparison cannot accept a neighbor when the multiplier is zero or negative. + +**Affected users:** Users whose converted pipelines contain a zero or negative `MultiplesOfAverage` value, or who supply a non-positive absolute radius in SIMPLNX. + +**Recommendation:** Trust SIMPLNX. Supply a positive value for the selected radius mode. + +**Evidence:** Current `ComputeNeighborhoodsFilter::preflightImpl()` contains both positivity checks. `SimplnxCore::ComputeNeighborhoods_InvalidSearchRadius` asserts both error codes with zero values. DREAM.3D v6.5.171 `FindNeighborhoods::dataCheck()` contains no equivalent check and has no absolute-radius mode. --- -## ComputeNeighborhoodsFilter-D2 +## Part 2 — SIMPLNX defects and behavior changes + +### ComputeNeighborhoodsFilter-D4 | Field | Value | |---|---| -| **Deviation ID** | `ComputeNeighborhoodsFilter-D2` | +| **Deviation ID** | `ComputeNeighborhoodsFilter-D4` | | **Filter UUID** | `924c10e3-2f39-4c08-9d7a-7fe029f74f6d` | | **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:** The SIMPLNX filter has no "Feature Phases" input parameter; 6.5.171 `FindNeighborhoods` required one. +**Symptom:** Multiples mode used the same radius for every feature and changed neighbor membership relative to each feature's own diameter. -**Root cause:** *Algorithmic choice (API).* `FindNeighborhoods` declared `FeaturePhases` as a required array in `dataCheck()` but never referenced it in the neighbor computation (`execute()` / `FindNeighborhoodsImpl`). SIMPLNX removes the unused parameter. Output arrays are unaffected. +**Root cause:** Bug. The Euclidean rewrite used `averageDiameter * multiplier / 2` for every feature. It removed the per-feature reach and introduced a division by two. The corrected algorithm uses `equivalentDiameter[i] * multiplier`; average diameter sizes the search grid only. -**Affected users:** Anyone converting a 6.5.171/SIMPL pipeline — the `FeaturePhasesArrayPath` key is dropped during conversion. No change to computed `Neighborhoods` / `NeighborhoodList` values. +**Affected users:** Users of multiples mode in DREAM3D-NX v7.4.1. Large features could lose neighbors because the radius no longer followed their own diameter. -**Recommendation:** *Trust SIMPLNX.* The removed input had no effect on output; dropping it removes a confusing required selection (and it was never needed by the absolute "Search Radius (microns)" mode either). +**Recommendation:** Trust SIMPLNX in DREAM3D-NX v7.4.2 or later. Recompute neighborhood arrays produced by the affected release. + +**Evidence:** Direct inspection of `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeNeighborhoods.cpp` on all release branches finds per-feature bin limits in `upstream/release/v7.0.0` through `upstream/release/v7.4.0`, the global half-diameter radius in `upstream/release/v7.4.1`, and per-feature radii in `upstream/release/v7.4.2`. PR #1485 (`f34c38916`) introduced the regression; PR #1652 (`d967a166a`) fixed it. DREAM.3D tag `v6.5.171`, `Source/Plugins/Statistics/StatisticsFilters/FindNeighborhoods.cpp`, uses per-feature limits. `SimplnxCore::ComputeNeighborhoods_MultiplesAnalyticalOracle` asserts counts `{0,3,0,0,1,0}` and asymmetric membership. All ordinary tests pass in CI at `419f9db35`. --- -## ComputeNeighborhoodsFilter-D3 +### ComputeNeighborhoodsFilter-D5 | Field | Value | |---|---| -| **Deviation ID** | `ComputeNeighborhoodsFilter-D3` | +| **Deviation ID** | `ComputeNeighborhoodsFilter-D5` | | **Filter UUID** | `924c10e3-2f39-4c08-9d7a-7fe029f74f6d` | | **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:** Background feature 0 could have a nonzero neighborhood count while its output neighbor list stayed empty. -**Symptom:** SIMPLNX rejects non-positive radius parameters at preflight; 6.5.171 accepted any value. A converted pipeline with `MultiplesOfAverage ≤ 0` now fails preflight with error `-5732` (multiples mode) and a non-positive search radius fails with `-5733` (microns mode). +**Root cause:** Bug. The Euclidean rewrite started the source range at 0 without the earlier worker guard that skipped background. It could find real features near the background centroid. Final list writes still started at feature 1. The corrected algorithm starts source searches at feature 1 and writes an explicit empty list for feature 0. Candidate insertion also excludes feature 0. -**Root cause:** *Intentional hardening.* A non-positive multiplier/radius produces an empty or degenerate search region — under 6.5.171 it silently produced all-zero `Neighborhoods` output. SIMPLNX surfaces the misconfiguration at preflight instead of silently returning meaningless results. +**Affected users:** Users of DREAM3D-NX v7.4.1 whose background centroid is near a real feature. Background counts could disagree with the corresponding list size. -**Affected users:** Only pipelines carrying a zero or negative `MultiplesOfAverage` value, which produced no useful output in 6.5.171 anyway. +**Recommendation:** Trust SIMPLNX in DREAM3D-NX v7.4.2 or later. Recompute affected arrays so background has zero count and an empty list. -**Recommendation:** *Trust SIMPLNX.* Set a positive multiplier/radius. Both error paths are pinned by `ComputeNeighborhoods_InvalidSearchRadius`. +**Evidence:** The same release algorithm files checked for D4 show an explicit `start == 0` guard through `upstream/release/v7.4.0`. `upstream/release/v7.4.1` has `Range(0, totalFeatures)` without that guard and writes lists from index 1. `upstream/release/v7.4.2` has `Range(1, totalFeatures)` and writes lists from index 0. PR #1652 (`d967a166a`) includes the background correction. DREAM.3D v6.5.171 `FindNeighborhoodsImpl::convert()` skips source 0. Both analytical tests place background at feature 1's centroid and assert its zero count and empty list. From c82d47a72bbe2fc773dbed3935f611d0608fbae3 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 20:01:28 -0400 Subject: [PATCH 48/91] DOC: Rewrite the ComputeTriangleGeomCentroids V&V record to follow the report template Signed-off-by: Michael Jackson --- .../vv/ComputeTriangleGeomCentroidsFilter.md | 110 +++++++++++------- .../ComputeTriangleGeomCentroidsFilter.md | 76 +++++++++--- .../vv/provenance/12_IN625_GBCD.md | 45 ++++--- 3 files changed, 157 insertions(+), 74 deletions(-) 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/deviations/ComputeTriangleGeomCentroidsFilter.md b/src/Plugins/SimplnxCore/vv/deviations/ComputeTriangleGeomCentroidsFilter.md index 3369ef4199..eaf1c76373 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/ComputeTriangleGeomCentroidsFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/ComputeTriangleGeomCentroidsFilter.md @@ -1,49 +1,89 @@ -# Deviations from DREAM3D 6.5.171: ComputeTriangleGeomCentroidsFilter +# Deviations from DREAM.3D v6.5.171: ComputeTriangleGeomCentroidsFilter -This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent. +Part 1 records differences from `FindTriangleGeomCentroids` in the SurfaceMeshing plugin. Part 2 records the two fixed SIMPLNX periodic defects. Both parts use one stable ID sequence. -Entries are referenced by stable ID (`ComputeTriangleGeomCentroidsFilter-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 -Legacy equivalent: `FindTriangleGeomCentroids` (DREAM3D 6.5.171, SurfaceMeshing plugin). Comparison method: source inspection of `FindTriangleGeomCentroids.cpp` against the SIMPLNX algorithm; the non-periodic path is a line-for-line match, so no empirical binary A/B was required (it can be run for bit-confirmation if desired). +### ComputeTriangleGeomCentroidsFilter-D1 + +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeTriangleGeomCentroidsFilter-D1` | +| **Filter UUID** | `074c0523-ab7a-4097-b0c3-c4dcbfb9a05e` | +| **Status** | active | + +**Symptom:** SIMPLNX offers an `Is Periodic` option that DREAM.3D v6.5.171 does not provide. + +**Root cause:** Algorithmic choice. Both filters calculate the arithmetic mean of each feature's unique vertices. SIMPLNX can also adjust an axis when a feature touches both opposing geometry faces. It calculates a minimum-image mean around the largest empty gap in the vertex coordinates. If the largest gaps tie, it keeps the arithmetic mean. The option defaults to false. + +**Affected users:** Users who enable `Is Periodic` for a surface mesh that wraps across domain boundaries. + +**Recommendation:** Trust SIMPLNX. The analytical fixture checks symmetric wrapping, asymmetric wrapping, and the tied-gap fallback. DREAM.3D v6.5.171 has no periodic output for comparison. + +**Evidence:** The DREAM.3D `v6.5.171` source at `Source/Plugins/SurfaceMeshing/SurfaceMeshingFilters/FindTriangleGeomCentroids.cpp` has no periodic parameter or adjustment. `Periodic Minimum-Image Oracle` in `src/Plugins/SimplnxCore/test/ComputeTriangleGeomCentroidsTest.cpp` checks both settings against literal centroid values. --- -## ComputeTriangleGeomCentroidsFilter-D1 +### ComputeTriangleGeomCentroidsFilter-D2 | Field | Value | |---|---| -| **Deviation ID** | `ComputeTriangleGeomCentroidsFilter-D1` | +| **Deviation ID** | `ComputeTriangleGeomCentroidsFilter-D2` | | **Filter UUID** | `074c0523-ab7a-4097-b0c3-c4dcbfb9a05e` | | **Status** | active | -**Symptom:** SIMPLNX exposes an `Is Periodic` option that DREAM3D 6.5.171 does not; when enabled, features that wrap the domain boundary receive a different (and correct) centroid than the naive arithmetic mean. +**Symptom:** SIMPLNX grows an undersized feature Attribute Matrix. DREAM.3D v6.5.171 reports `-99500` for a positive face label greater than the matrix tuple count; a label equal to that count receives no centroid. -**Root cause:** Algorithmic choice (SIMPLNX-only feature). Legacy `FindTriangleGeomCentroids` has no periodic mode — it always reports the plain arithmetic mean of a feature's unique vertices. SIMPLNX adds `Is Periodic`; for any axis on which a feature touches both opposing periodic faces, the centroid component is computed as a minimum-image (largest-empty-gap) mean of the feature's vertex coordinates (`GeometryHelpers.cpp`, `AdjustCentroidsForPeriodicFaces` `BoundingBox3Df` overload) instead of the arithmetic mean. With `Is Periodic = false` (the default) SIMPLNX reproduces the legacy result. +**Root cause:** Algorithmic choice. With N feature tuples, DREAM.3D v6.5.171 allocates N + 1 vertex sets and rejects labels at least N + 1. It calculates centroids only for indices below N. Thus label N enters a set but has no output tuple. SIMPLNX grows the matrix to `maxFeatureId + 1` tuples before gathering vertices and returns an error if resizing fails. -**Affected users:** Only users who enable `Is Periodic` on a periodic surface mesh with grains that wrap the boundary. The default (off) path is unaffected and matches 6.5.171. +**Affected users:** Users with a positive face label greater than or equal to the feature-matrix tuple count. -**Recommendation:** Trust SIMPLNX. 6.5.171 has no comparable output; the SIMPLNX periodic centroid is verified against an independent Class 1 analytical oracle (see the report's Oracle section). +**Recommendation:** Trust SIMPLNX. It provides an output tuple for each positive label. Add a separate size check if a pipeline requires rejection of an undersized matrix. + +**Evidence:** DREAM.3D tag `v6.5.171`, `Source/Plugins/SurfaceMeshing/SurfaceMeshingFilters/FindTriangleGeomCentroids.cpp`, contains the N + 1 guard and the N-bound output loop. Current `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeTriangleGeomCentroids.cpp` contains the resize and error propagation. Neither current test directly exercises the resize. --- -## ComputeTriangleGeomCentroidsFilter-D2 +## Part 2 — SIMPLNX defects and behavior changes + +### ComputeTriangleGeomCentroidsFilter-D3 | Field | Value | |---|---| -| **Deviation ID** | `ComputeTriangleGeomCentroidsFilter-D2` | +| **Deviation ID** | `ComputeTriangleGeomCentroidsFilter-D3` | | **Filter UUID** | `074c0523-ab7a-4097-b0c3-c4dcbfb9a05e` | | **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.2.1 through v7.4.2. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | The DREAM3D-NX release after v7.4.2 | + +**Symptom:** Periodic adjustment could give an incorrect centroid outside the geometry bounds. -**Symptom:** When a face label is larger than the number of tuples in the target Feature Attribute Matrix, 6.5.171 aborts with error `-99500`; SIMPLNX instead grows the Feature Attribute Matrix to fit and completes successfully. +**Root cause:** Bug. The adjustment added half the domain length to the arithmetic mean without considering the vertex distribution. On X in [0,4], vertices at `{0,3,3.5,4}` gave 4.625 instead of 3.625. The filter now uses the largest-empty-gap minimum-image mean and maps the result into the domain. Equal largest gaps select the arithmetic mean. -**Root cause:** Algorithmic choice. Legacy sizes `vertexSets` to `numFeatures + 1` and errors (`-99500`) if any label meets or exceeds that bound (`FindTriangleGeomCentroids.cpp:185-192`). SIMPLNX resizes the Feature Attribute Matrix to `maxFeatureId + 1` tuples up front (`ComputeTriangleGeomCentroids.cpp:48-52`) and proceeds. The centroid values that both produce for in-range labels are identical. +**Affected users:** Users who enable `Is Periodic` for features that touch both opposing faces on an axis. DREAM3D-NX v7.0.0 through v7.2.0 and DREAM.3D v6.5.171 have no periodic path. -**Affected users:** Anyone who supplied a Feature Attribute Matrix smaller than `max(faceLabel) + 1`. Under 6.5.171 the run failed; under SIMPLNX it succeeds with an enlarged matrix. +**Recommendation:** Use the DREAM3D-NX release after v7.4.2 for periodic centroids. Disabling periodic mode avoids this adjustment but gives the ordinary arithmetic mean. -**Recommendation:** Either acceptable. SIMPLNX is strictly more permissive; the computed centroids agree for all valid labels. Users porting a pipeline that relied on the `-99500` error as a validation gate should add an explicit size check. +**Evidence:** All release sources from `upstream/release/v7.2.1` through `upstream/release/v7.4.2` add the constant offset in `src/simplnx/Utilities/GeometryHelpers.cpp`. PR #1670 (`4a7b71e5f`) replaces it with `PeriodicMean1D`. The `Periodic Minimum-Image Oracle` test checks F3 = `(3.625,3,0)` and geometry bounds; all pass in CI at `419f9db35`. --- -## Note on the periodic bug fix (internal, not a legacy deviation) +### ComputeTriangleGeomCentroidsFilter-D4 + +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeTriangleGeomCentroidsFilter-D4` | +| **Filter UUID** | `074c0523-ab7a-4097-b0c3-c4dcbfb9a05e` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.2.1 through v7.4.2. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | The DREAM3D-NX release after v7.4.2 | + +**Symptom:** Periodic adjustment could stop with `std::bad_function_call` when no message callback was installed. + +**Root cause:** Bug. After periodic adjustment, the algorithm invoked `m_MessageHandler.m_Callback` directly. An empty callback throws. The filter now uses `sendMessage`, which checks the callback before invoking it. + +**Affected users:** API users who execute periodic adjustment without a message callback. DREAM3D-NX v7.0.0 through v7.2.0 and DREAM.3D v6.5.171 have no periodic path. + +**Recommendation:** Use the DREAM3D-NX release after v7.4.2. In affected releases, install a message callback to avoid this exception; this does not correct D3. -Prior to this V&V, SIMPLNX's periodic path added a **constant `|max − min| / 2` offset** to the naive centroid (issue #1665). That is only correct for a feature whose mass is symmetric about the seam and can place the centroid outside the domain for asymmetric wrapped features. It was corrected to the minimum-image mean described in D1. This is a SIMPLNX-internal correction, **not** a deviation from 6.5.171 (which has no periodic path), so it is recorded here only for provenance. +**Evidence:** `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeTriangleGeomCentroids.cpp` directly invokes the callback in all release sources from `upstream/release/v7.2.1` through `upstream/release/v7.4.2`. PR #1670 (`4a7b71e5f`) adds the guarded call. `Periodic Minimum-Image Oracle` executes with no callback argument and verifies successful output; all pass in CI at `419f9db35`. diff --git a/src/Plugins/SimplnxCore/vv/provenance/12_IN625_GBCD.md b/src/Plugins/SimplnxCore/vv/provenance/12_IN625_GBCD.md index 2453876024..5871194cbd 100644 --- a/src/Plugins/SimplnxCore/vv/provenance/12_IN625_GBCD.md +++ b/src/Plugins/SimplnxCore/vv/provenance/12_IN625_GBCD.md @@ -1,8 +1,6 @@ # Exemplar Archive Provenance: 12_IN625_GBCD.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?" - ---- +This sidecar records the shared archive and the independent centroid oracle for `ComputeTriangleGeomCentroidsFilter`. ## Archive identity @@ -10,31 +8,52 @@ This sidecar records how an exemplar archive used in unit tests was generated. I |---|---| | **Archive** | `12_IN625_GBCD.tar.gz` | | **SHA512** | `f696a8af181505947e6fecfdb1a11fda6c762bba5e85fea8d484b1af00bf18643e1d930d48f092ee238d1c19c9ce7c4fb5a8092d17774bda867961a1400e9cea` | -| **Used by tests** | `SimplnxCore::ComputeTriangleGeomCentroids` (non-periodic exemplar); also shared by `ComputeTriangleGeomShapes`, `ComputeTriangleGeomVolumes`, `SharedFeatureFace`. | -| **Generated by** | Pre-existing BlueQuartz IN625 GBCD dataset (predates this V&V). | -| **Generated on** | Pre-existing. | -| **Generated at commit** | Pre-existing (declared in `src/Plugins/OrientationAnalysis/test/CMakeLists.txt`; not re-generated by this V&V). | +| **Used by tests** | `SimplnxCore::ComputeTriangleGeomCentroids`; shared with other surface-mesh tests. | +| **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 first appears in a plugin test CMake file in PR #399. Its current registration is in `src/Plugins/OrientationAnalysis/test/CMakeLists.txt`, rather than SimplnxCore. The registered SHA512 matches the archive bytes and the value above. ## How it was generated -The archive is a pre-existing IN625 grain-boundary-character-distribution surface mesh shipped in the BlueQuartz Data_Archive. This V&V did **not** regenerate or modify it. The `ComputeTriangleGeomCentroids` unit test loads it, runs the filter with `Is Periodic = false`, and compares the generated `Centroids [NX Computed]` array against the `Centroids` array already stored in the file. +The archive contains `12_IN625_GBCD.dream3d`, `.vtk`, and `.xdmf` files. The embedded `/Pipeline/Pipeline` in the `.dream3d` file records a local proof version importing `01_IN625_RawEBSD.h5ebsd`, reconstructing features, creating and smoothing a triangle mesh, and extracting internal surfaces. Step 30 runs `FindTriangleGeomCentroids` on `TriangleDataContainer/FaceData/FaceLabels` and writes `FaceFeatureData/Centroids`; step 34 writes the data file. The source input is EBSD data, not a hand-constructed analytical fixture. + +The archived face-label array has 1,874,216 tuples and two components. Its first component includes -1; all second-component labels are positive. The maximum label is 819, and the centroid array has 820 tuples with three components, so the exemplar does not exercise feature-matrix growth. ## Canonical oracle output | DataPath | Source of expected values | |---|---| -| `.../FaceFeatureData/Centroids` (reference array in the archive) | **Regression pin only** — this reference array is prior DREAM3D output (a circular oracle for the non-periodic path). | +| `TriangleDataContainer/FaceFeatureData/Centroids` | Regression reference from the embedded local-proof pipeline, step 30. | +| `TriangleDataContainer/FaceFeatureData/Centroids [NX Computed]` | Class 1 literal expectations in the constructed mesh test; Class 4 geometry-bound checks. This output is created by the test, not stored in the archive. | + +The independent oracle is `SimplnxCore::ComputeTriangleGeomCentroids: Periodic Minimum-Image Oracle` in `src/Plugins/SimplnxCore/test/ComputeTriangleGeomCentroidsTest.cpp`. `BuildPeriodicToyMesh()` constructs 16 vertices and eight triangles. Each feature uses a distinct set of vertex indices; shared triangle vertices count once. Distinct vertices at the same position still count separately. -The **primary** oracle for this filter is **not** this archive. Correctness of both the non-periodic and periodic paths is established independently by the inline **Class 1 (Analytical) + Class 4 (Invariant)** fixtures in `test/ComputeTriangleGeomCentroidsTest.cpp::"Periodic Minimum-Image Oracle"`. This exemplar test is retained only as a large-mesh regression pin for the non-periodic path; it does not exercise the periodic branch. +| Feature | X coordinates | Y, Z | Non-periodic X mean | Periodic X mean | +|---|---|---|---|---| +| F0 | Empty | Output `(0,0,0)` | 0 | 0 | +| F1 | `{1,2,3}` | `1,0` | 2 | 2 | +| F2 | `{0,0,4,4}` | `2,0` | 2 | 0 | +| F3 | `{0,3,3.5,4}` | `3,0` | 2.625 | 3.625 | +| F4 | `{0,1,2,3,4}` | `1,0` | 2 | 2 | + +The geometry bounds are X = [0,4], Y = [1,3], and Z = [0,0]. Only X can touch both opposing faces in this fixture. For F3, unwrap the seam coordinates to `{4,3,3.5,4}`; their mean is 3.625. F4 has tied largest gaps and uses the arithmetic mean. Each mode checks all 15 centroid components within absolute tolerance `1e-4`, all populated-feature bounds, and tuple dimensions. + +All pass in CI at `419f9db35` on Linux, macOS, and Windows. The current test, algorithm, and `GeometryHelpers.cpp` match that source snapshot. ## Oracle provenance (Classes 2, 3, 5 only) -Not applicable. The independent oracle is Class 1 / Class 4 and lives directly in the test code (no archive-based oracle). +Not applicable. Classes 1 and 4 supply the independent oracle in the test code. The archive provides a separate non-periodic regression comparison. + +The source comparison uses DREAM.3D tag `v6.5.171`, `Source/Plugins/SurfaceMeshing/SurfaceMeshingFilters/FindTriangleGeomCentroids.cpp`. It confirms the unique-vertex arithmetic mean, absence of periodic adjustment, and the N + 1 face-label guard followed by an N-bound output loop. No runtime comparison artifacts are available at `vv_work:ComputeTriangleGeomCentroidsFilter/`. + +Release-source inspection covers all ten `upstream/release/v` references from v7.0.0 through v7.4.2. `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeTriangleGeomCentroids.cpp` has no periodic path through v7.2.0. Releases v7.2.1 through v7.4.2 have the direct callback invocation and the constant-offset call; their `src/simplnx/Utilities/GeometryHelpers.cpp` adds half the domain length. PR #1670 (`4a7b71e5f`) fixes both defects, recorded as `ComputeTriangleGeomCentroidsFilter-D3` and `ComputeTriangleGeomCentroidsFilter-D4`. Both fixes will be in the DREAM3D-NX release after v7.4.2. ## Second-engineer oracle review -Pending — combined with the V&V PR review (see report Oracle section). +Pending, as recorded in the report sign-off. ## Regenerated to fix a circular-oracle situation? -No. The archive is unchanged. The circular reference `Centroids` array is explicitly demoted to a regression pin here; the authoritative oracle is the inline analytical fixture, so no regeneration was necessary. +No. The archive remains unchanged. The analytical fixture supplies independent expectations for both periodic settings; the archived centroid array supplies a regression reference only. From 6b2df65cffa489eaa9700b00bd124c8c3c85c299 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 20:01:28 -0400 Subject: [PATCH 49/91] DOC: Rewrite the CopyFeatureArrayToElementArray V&V record to follow the report template Signed-off-by: Michael Jackson --- .../CopyFeatureArrayToElementArrayFilter.md | 197 +++++++++--------- .../CopyFeatureArrayToElementArrayFilter.md | 114 ++++++++-- 2 files changed, 189 insertions(+), 122 deletions(-) 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/deviations/CopyFeatureArrayToElementArrayFilter.md b/src/Plugins/SimplnxCore/vv/deviations/CopyFeatureArrayToElementArrayFilter.md index c3e1f9388e..08d10e7081 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/CopyFeatureArrayToElementArrayFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/CopyFeatureArrayToElementArrayFilter.md @@ -1,14 +1,12 @@ -# Deviations from DREAM3D 6.5.171: CopyFeatureArrayToElementArrayFilter +# Deviations from DREAM.3D v6.5.171: CopyFeatureArrayToElementArrayFilter -This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent (`CopyFeatureArrayToElementArray`, SIMPL UUID `99836b75-144b-5126-b261-b411133b5e8a`). - -Entries are referenced by stable ID (`CopyFeatureArrayToElementArrayFilter-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. - -**Numeric output on valid input is bit-identical** — A/B comparison run 2026-07-23 on a 4×3×1 / 4-feature fixture with float32 (1-comp), int32 (3-comp), and bool arrays showed all output arrays bit-identical between 6.5.171 and SIMPLNX. All deviations below concern *naming* and *input-validation* behavior, not numeric results. +Part 1 lists differences from DREAM.3D v6.5.171. Part 2 lists SIMPLNX defects fixed in DREAM3D-NX v7.4.2. All entries share one stable ID sequence. --- -## CopyFeatureArrayToElementArrayFilter-D1 +## Part 1 — Differences from DREAM.3D v6.5.171 + +### CopyFeatureArrayToElementArrayFilter-D1 | Field | Value | |---|---| @@ -16,17 +14,19 @@ Entries are referenced by stable ID (`CopyFeatureArrayToElementArrayFilter-D` | **Filter UUID** | `4c8c976a-993d-438b-bd8e-99f71114b9a1` | | **Status** | active | -**Symptom:** Pipelines converted from legacy DREAM3D produce output arrays with different *names*: legacy names the created array exactly what the user typed (e.g., `MyOutput`); SIMPLNX names it `` (the legacy name string is converted into the suffix, so a legacy pipeline copying `EquivalentDiameters` to `MyOutput` produces `EquivalentDiametersMyOutput` in SIMPLNX). +**Symptom:** A converted pipeline uses the source array name for the output in SIMPLNX. DREAM.3D v6.5.171 uses the explicit `CreatedArrayName` value. -**Root cause:** Algorithmic choice. The SIMPLNX filter was redesigned from single-array-with-explicit-name to multi-array-with-shared-suffix (`MultiArraySelectionParameter` + `created_array_suffix`). `FromSIMPLJson()` maps legacy `CreatedArrayName` onto the suffix (`CopyFeatureArrayToElementArrayFilter.cpp`, `FromSIMPLJson`), which cannot reproduce the legacy naming for a non-empty name. +**Root cause:** Algorithmic choice. SIMPLNX selects multiple feature arrays and appends one shared suffix to their names. In DREAM3D-NX v7.4.2, `FromSIMPLJson()` leaves that suffix empty. For example, copying `EquivalentDiameters` to `MyOutput` in DREAM.3D creates `EquivalentDiameters` after conversion. DREAM3D-NX v7.0.0 through v7.4.1 instead converted the explicit name to a suffix and created `EquivalentDiametersMyOutput`. -**Affected users:** Anyone importing a legacy `.json` pipeline containing this filter. Downstream filters in the converted pipeline that reference the legacy output-array name will fail preflight until the user updates the reference (or renames via a Rename Data Object filter). +**Affected users:** Users who import a DREAM.3D pipeline with an output name different from its source name. Downstream references to the explicit legacy name must be updated. -**Recommendation:** Trust SIMPLNX. Numeric content is identical; after conversion, update downstream array references to the new `` name. +**Recommendation:** Trust SIMPLNX. Update downstream references to the source array name, or rename the output before downstream processing. + +**Evidence:** `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/CopyFeatureArrayToElementArrayFilter.cpp::FromSIMPLJson()` leaves the suffix empty. `SIMPL Backwards Compatibility` asserts this for both conversion fixtures. The source at each `upstream/release/v7.0.0` through `upstream/release/v7.4.1` maps `CreatedArrayName` to the suffix; `upstream/release/v7.4.2` contains the correction from PR #1682 (`5eb69f359`). The legacy `Source/SIMPLib/CoreFilters/CopyFeatureArrayToElementArray.cpp` calls `setName(getCreatedArrayName())`. --- -## CopyFeatureArrayToElementArrayFilter-D2 +### CopyFeatureArrayToElementArrayFilter-D2 | Field | Value | |---|---| @@ -34,19 +34,19 @@ Entries are referenced by stable ID (`CopyFeatureArrayToElementArrayFilter-D` | **Filter UUID** | `4c8c976a-993d-438b-bd8e-99f71114b9a1` | | **Status** | active | -**Symptom:** When the selected Feature array has more tuples than `largestFeatureId + 1` (an over-provisioned Feature AttributeMatrix), DREAM3D 6.5.171 aborts with error -5555 ("The number of Features in the InArray array (N) does not match the largest Feature Id"); SIMPLNX runs successfully. +**Symptom:** DREAM.3D v6.5.171 returns error `-5555` when a feature array has more tuples than the largest FeatureId plus one. SIMPLNX accepts these unused feature tuples. -**Root cause:** Algorithmic choice. Legacy `execute()` required `largestFeatureId == numFeatures - 1` exactly (SIMPL `CopyFeatureArrayToElementArray.cpp:235-241`). SIMPLNX (`ValidateFeatureIdsToFeatureAttributeMatrixIndexing`, `DataArrayUtilities.cpp:160-198`) only rejects ids that would index *past* the array (`maxFeatureId >= numFeatures`, error -5351). Extra unused feature tuples are legal in SIMPLNX — they are simply never read. +**Root cause:** Algorithmic choice. DREAM.3D requires `largestFeatureId == numFeatures - 1`. SIMPLNX rejects only negative identifiers and identifiers at or above `numFeatures`. Unused feature tuples do not affect the values copied to cells. -Confirmed by A/B probe 2026-07-23: 6-cell fixture with max id 2 and an 8-tuple feature array — 6.5.171 errored -5555; SIMPLNX produced correct output for all referenced tuples (verified against hand-derived values; also pinned by unit test `Over-provisioned Feature array accepted`). +**Affected users:** Users whose feature arrays retain entries for features absent from the cell FeatureIds, such as after cropping. -**Affected users:** Users whose Feature AttributeMatrix retains tuples for features no longer present in the FeatureIds map (e.g., after feature removal/cropping). Their legacy pipelines failed; the same data now processes in SIMPLNX. +**Recommendation:** Trust SIMPLNX. Every referenced tuple is within the source array, and unused tuples need not be removed. -**Recommendation:** Trust SIMPLNX. The legacy strict-equality check rejected valid input; the SIMPLNX output for all referenced feature ids is well-defined and verified. +**Evidence:** `Over-provisioned Feature array accepted` checks six literal outputs from eight feature tuples with maximum ID 2. `vv_work:CopyFeatureArrayToElementArrayFilter/comparison/results.md` records a stock DREAM.3D v6.5.171 run on 2026-07-23: all three main-fixture output arrays match exactly, while the extra-feature probe returns `-5555` only in DREAM.3D. `comparison/make_input.py`, `comparison/compare.py`, and the `simpl_main.json`, `nx_main.d3dpipeline`, `simpl_overprov.json` and `nx_overprov.d3dpipeline` files under `vv_work:CopyFeatureArrayToElementArrayFilter/` define that comparison. These results apply to the algorithm used on that date. Subsequent algorithm-source changes are `2b7f72ca5` (2026-08-05), `4ec21e93f` (2026-09-20) and `a33c370ca` (2026-09-25). --- -## CopyFeatureArrayToElementArrayFilter-D3 +### CopyFeatureArrayToElementArrayFilter-D3 | Field | Value | |---|---| @@ -54,12 +54,80 @@ Confirmed by A/B probe 2026-07-23: 6-cell fixture with max id 2 and an 8-tuple f | **Filter UUID** | `4c8c976a-993d-438b-bd8e-99f71114b9a1` | | **Status** | active | -**Symptom:** When the FeatureIds array contains a negative value, DREAM3D 6.5.171 **completes without any error** and writes an undefined value at the affected cells; SIMPLNX stops with error -5355 ("Feature Ids array … has negative values"). +**Symptom:** A negative FeatureId can make DREAM.3D v6.5.171 read outside the feature array and produce an undefined value. SIMPLNX returns error `-5355` before copying values. + +**Root cause:** Bug. DREAM.3D v6.5.171 was affected: its maximum-ID check does not reject negatives, and `copyData()` uses the identifier in an unchecked pointer offset. DREAM3D-NX v7.0.0 through v7.4.2 were not affected. Their validation rejects negative identifiers before the copy. + +**Affected users:** Users with negative sentinel values, such as `-1` for an unassigned cell, in FeatureIds. + +**Recommendation:** Trust SIMPLNX. Replace negative sentinels with valid feature identifiers before execution. Undefined legacy values are not valid feature data. + +**Evidence:** `vv_work:CopyFeatureArrayToElementArrayFilter/comparison/results.md` records the 2026-07-23 negative-ID probe: DREAM.3D completes and writes an observed zero at the affected cell, while SIMPLNX returns `-5355`. The probe pipelines are `vv_work:CopyFeatureArrayToElementArrayFilter/comparison/simpl_negid.json` and `vv_work:CopyFeatureArrayToElementArrayFilter/comparison/nx_negid.d3dpipeline`. `Execute Error - Negative FeatureIds (-5355)` checks the current error and all zero-initialized output values. Each release source uses either `ValidateNumFeaturesInArray()` (v7.0.0 through v7.2.0) or `ValidateFeatureIdsToFeatureAttributeMatrixIndexing()` (v7.2.1 through v7.4.2), both with negative-ID rejection. The legacy source is `Source/SIMPLib/CoreFilters/CopyFeatureArrayToElementArray.cpp` at SIMPL `0afaa6854ac0ef27df17278e11be711f74f77b90`, the last revision on or before the DREAM.3D `v6.5.171` tag date, 2026-05-27. + +--- + +## Part 2 — SIMPLNX defects and behavior changes + +### CopyFeatureArrayToElementArrayFilter-D4 + +| Field | Value | +|---|---| +| **Deviation ID** | `CopyFeatureArrayToElementArrayFilter-D4` | +| **Filter UUID** | `4c8c976a-993d-438b-bd8e-99f71114b9a1` | +| **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:** Selecting a NeighborList or StringArray can throw `std::bad_cast` during preflight instead of returning a validation error. + +**Root cause:** Bug. The selection parameter accepted any array type, but preflight obtained each selected object as `IDataArray`. NeighborList and StringArray do not meet that requirement. The filter now permits only `IArray::ArrayType::DataArray`. DREAM.3D has no corresponding reference-cast path; its copy routine checks supported primitive types. + +**Affected users:** Users who select a non-DataArray as feature data in DREAM3D-NX v7.0.0 through v7.4.1. + +**Recommendation:** Trust SIMPLNX. Use DREAM3D-NX v7.4.2 or later to receive a validation error for an unsupported selection. + +**Evidence:** PR #1689 (`2b7f72ca5`) restricts the parameter type. Each release source from `upstream/release/v7.0.0` through `upstream/release/v7.4.1` uses `ArrayType::Any`; `upstream/release/v7.4.2` uses `ArrayType::DataArray`. `Preflight Error - Non-DataArray selection rejected` checks a NeighborList. The legacy source cited in D3 uses checked dynamic casts and reports unsupported primitive types. + +--- + +### CopyFeatureArrayToElementArrayFilter-D5 + +| Field | Value | +|---|---| +| **Deviation ID** | `CopyFeatureArrayToElementArrayFilter-D5` | +| **Filter UUID** | `4c8c976a-993d-438b-bd8e-99f71114b9a1` | +| **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 slash in the created-array suffix can throw `std::invalid_argument` instead of returning a preflight error. + +**Root cause:** Bug. The filter combined the unchecked suffix with the source name and constructed an invalid output DataPath. Preflight now checks for `/` and returns error `-3021`. DREAM.3D has no suffix parameter or this DataPath construction path. + +**Affected users:** Users who enter `/` in the suffix in DREAM3D-NX v7.0.0 through v7.4.1. + +**Recommendation:** Trust SIMPLNX. Remove `/` from the suffix. DREAM3D-NX v7.4.2 or later reports the invalid suffix before creating an output path. + +**Evidence:** PR #1689 (`2b7f72ca5`) adds the slash check. It is absent from each release source through `upstream/release/v7.4.1` and present in `upstream/release/v7.4.2`. `Preflight Error - Suffix contains '/' (-3021)` checks the diagnostic. + +--- + +### CopyFeatureArrayToElementArrayFilter-D6 + +| Field | Value | +|---|---| +| **Deviation ID** | `CopyFeatureArrayToElementArrayFilter-D6` | +| **Filter UUID** | `4c8c976a-993d-438b-bd8e-99f71114b9a1` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.2.1 through v7.4.1. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | + +**Symptom:** Empty FeatureIds can cause an invalid memory read or a crash during execution. -**Root cause:** Bug in 6.5.171. Legacy `copyData()` computes `fPtr + (numComp * featureIdx)` with no lower-bound check (SIMPL `CopyFeatureArrayToElementArray.cpp:178-189`); a negative id reads out of bounds before the feature array's allocation — undefined behavior whose result depends on adjacent heap contents. The legacy max-id scan (`m_FeatureIds[i] > largestFeature`) only tracks the maximum, so negatives pass validation silently. SIMPLNX validates the minimum feature id before copying and refuses to run. +**Root cause:** Bug. The range validator dereferenced the iterators returned by `std::minmax_element()` even when the input was empty. The validator now returns success before that read, and the filter creates empty outputs. DREAM3D-NX v7.0.0 through v7.2.0 used a range loop that skipped empty input. DREAM.3D uses a count-controlled maximum-ID loop and has no empty-iterator dereference. -Confirmed by A/B probe 2026-07-23: fixture with `FeatureIds[5] = -1` — 6.5.171 reported "Pipeline Complete" and wrote 0.0 (whatever bytes preceded the allocation) at cell 5; SIMPLNX errored -5355 and produced no output. +**Affected users:** Users who copy feature values to an empty cell array in DREAM3D-NX v7.2.1 through v7.4.1. -**Affected users:** Anyone whose FeatureIds contains negative sentinel values (some workflows use -1 for "unassigned"). Legacy results at those cells were silent garbage. +**Recommendation:** Trust SIMPLNX. Use DREAM3D-NX v7.4.2 or later for empty cell arrays. -**Recommendation:** Trust SIMPLNX. The legacy behavior is undefined and platform-dependent; SIMPLNX's hard error is the correct response. Users with -1 sentinels must clean/reassign them before copying feature data down to cells. +**Evidence:** PR #1689 (`2b7f72ca5`) adds the empty-store guard to `src/simplnx/Utilities/DataArrayUtilities.cpp`. Direct source inspection finds the unsafe min/max dereference in each release from `upstream/release/v7.2.1` through `upstream/release/v7.4.1`, and the guard in `upstream/release/v7.4.2`. `Zero-tuple FeatureIds accepted` checks successful execution and zero output tuples. The legacy source cited in D3 has no min/max iterator reduction. From 4f0ba228313da8aebd839af9fc2d1f24444f344a Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 20:01:28 -0400 Subject: [PATCH 50/91] DOC: Rewrite the CreateFeatureArrayFromElementArray V&V record to follow the report template Signed-off-by: Michael Jackson --- ...reateFeatureArrayFromElementArrayFilter.md | 202 ++++++++---------- ...reateFeatureArrayFromElementArrayFilter.md | 135 +++++++----- ...reateFeatureArrayFromElementArrayFilter.md | 120 ++++++----- 3 files changed, 229 insertions(+), 228 deletions(-) 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/deviations/CreateFeatureArrayFromElementArrayFilter.md b/src/Plugins/SimplnxCore/vv/deviations/CreateFeatureArrayFromElementArrayFilter.md index 9969bb209a..cbbfcd05ad 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/CreateFeatureArrayFromElementArrayFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/CreateFeatureArrayFromElementArrayFilter.md @@ -1,86 +1,117 @@ -# Deviations from DREAM3D 6.5.171: CreateFeatureArrayFromElementArrayFilter +# Deviations from DREAM.3D v6.5.171: CreateFeatureArrayFromElementArrayFilter -This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent. +Part 1 lists feature-matrix sizing and input-validation differences from DREAM.3D v6.5.171. Part 2 lists the fixed empty-array and data-truncation defects in SIMPLNX. -Entries are referenced by stable ID (`CreateFeatureArrayFromElementArray-D`) from the V&V report and from public migration guidance. The Filter UUID field is the permanent cross-reference anchor. +The existing ID `CreateFeatureArrayFromElementArray-D1` is unchanged. New entries continue the same number sequence with the full filter name. The Filter UUID is the permanent cross-reference anchor. --- -## Headline +## Part 1 — Differences from DREAM.3D v6.5.171 -**One behavioral deviation identified — SIMPLNX improvement over SIMPL.** The per-cell copy loop is a genuine port and produces bit-identical output for all inputs that SIMPL can process. The single deviation is that SIMPLNX handles the under-sized AM case (where `max(featureIds) + 1 > AM.tupleCount`) by resizing the AM to accommodate, whereas SIMPL errors in that case. All pipelines that succeeded in SIMPL will succeed identically in SIMPLNX. +### CreateFeatureArrayFromElementArray-D1 ---- +| Field | Value | +|---|---| +| **Deviation ID** | `CreateFeatureArrayFromElementArray-D1` | +| **Filter UUID** | `50e1be47-b027-4f40-8f70-1283682ee3e7` | +| **Status** | active | -## Known deviations +**Symptom:** SIMPLNX accepts feature-matrix sizes that DREAM.3D v6.5.171 rejects. -### CreateFeatureArrayFromElementArray-D1 — AM under-sized: SIMPL errors; SIMPLNX resizes and succeeds +**Root cause:** Algorithmic choice. DREAM.3D v6.5.171 requires the matrix tuple count to equal the largest FeatureId plus one. A positive ID beyond the matrix gives error `-5555`; an oversized matrix gives error `-5556`. SIMPLNX grows an undersized matrix and its children after checking for child data loss. It preserves a matrix that already has enough tuples. For successful execution with nonnegative IDs, the output tuple count is `max(initial matrix tuple count, largest FeatureId + 1)`. -**Severity:** Low for migration — this deviation only occurs in configurations that SIMPL could not process at all (error -5555). No SIMPL pipeline that previously succeeded can produce this condition, so there is no output difference for any pipeline that ran to completion in SIMPL. +**Affected users:** Users whose feature matrix has fewer or more tuples than the FeatureId range requires. Growth can fail if a child array would lose tuples; see `CreateFeatureArrayFromElementArrayFilter-D5`. -**SIMPL behavior** (`execute()`, lines 226–252): -1. Reads `numFeatures = attributeMatrix.getNumberOfTuples()` — the pre-existing AM tuple count. -2. Scans all cells for `largestFeature = max(featureIds[:])`. -3. If `largestFeature >= numFeatures` → **error -5555** ("Attribute Matrix has N tuples but the input array has a Feature ID value of at least M"). No output is produced; the filter exits. -4. Otherwise → `copyCellData(..., numFeatures, ...)` creates a fresh `numFeatures`-tuple zero-initialized output array and fills it. AM tuple count is **never changed**. +**Recommendation:** Trust SIMPLNX. Size the matrix to the FeatureId range for compatibility with DREAM.3D v6.5.171. SIMPLNX retains larger matrices and sets unused output tuples to zero. -**SIMPLNX behavior** (`operator()()`, lines 84–112): -1. Computes `maxValue = max(featureIds[:])` via `std::max_element`. -2. If `maxValue < 0` → error -81880 (all-negative guard; SIMPL has undefined behavior in this case). -3. If `maxValue + 1 > cellFeatureAttrMat.getNumberOfTuples()`: - - Runs a shrink-protection loop over all AM children: if any child array has `getNumberOfTuples() > (maxValue + 1)` — meaning growing the AM to `maxValue + 1` would shrink that child — returns error -81881. Exercised by AF-5 (`test/CreateFeatureArrayFromElementArrayTest.cpp`). - - **Resizes the AM** via `cellFeatureAttrMat.resizeTuples({maxValue + 1})`. This cascades to all AM children (`AttributeMatrix::resizeTuples` iterates `findAllChildrenOfType()` and calls `array->resizeTuples(m_TupleShape)` on each), including the newly created output array. -4. Runs the copy loop. +**Evidence:** The legacy `Source/SIMPLib/CoreFilters/CreateFeatureArrayFromElementArray.cpp` contains both size errors. `vv_work:CreateFeatureArrayFromElementArrayFilter/results/legacy_oversized_run.log` records `-5556` on 2026-07-23. The SIMPLNX behavior in that run precedes the growth-only correction; current sizing is defined by `Algorithms/CreateFeatureArrayFromElementArray.cpp`. AF-1, AF-3 and AF-6 check growth; no current fixture checks an oversized matrix. -**Divergent outcomes by case:** +--- -| Case | Condition | SIMPL result | SIMPLNX result | -|---|---|---|---| -| **Exact match** | `max(featureIds) + 1 == AM.tupleCount` | SUCCESS — output has `AM.tupleCount` tuples | SUCCESS — resize block skipped; output has `AM.tupleCount` tuples (identical) | -| **AM over-sized** | `max(featureIds) + 1 < AM.tupleCount` | SUCCESS — output has `AM.tupleCount` tuples; trailing feature slots zero-filled | SUCCESS — resize block skipped; output has `AM.tupleCount` tuples; trailing slots retain `"0"` fill from `CreateArrayAction` (identical) | -| **AM under-sized** | `max(featureIds) + 1 > AM.tupleCount` | **ERROR -5555** — "Attribute Matrix has N tuples but Feature ID value is at least M" | **SUCCESS** — AM and all children (including output array) resized to `max(featureIds) + 1`; copy runs normally | +### CreateFeatureArrayFromElementArrayFilter-D2 -**Why the A/B comparison missed this:** -The synthetic fixture (`FeatureIds=[1,2,1,2,1,2,1,2]`, `max=2`) was paired with an AM of exactly 3 tuples — the Exact Match case. The AM under-sized case requires a pipeline configuration that SIMPL would have rejected at runtime, so it is not reachable from any SIMPL-generated test data. +| Field | Value | +|---|---| +| **Deviation ID** | `CreateFeatureArrayFromElementArrayFilter-D2` | +| **Filter UUID** | `50e1be47-b027-4f40-8f70-1283682ee3e7` | +| **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 | -**Migration impact:** None. Any pipeline that completed successfully in SIMPL had `max(featureIds) < numFeatures` by definition, placing it in the Exact Match or AM over-sized case — both of which produce identical output in SIMPLNX. The AM under-sized case is a new capability in SIMPLNX. +**Symptom:** A negative FeatureId could cause an access outside the output array and corrupt memory. ---- +**Root cause:** Bug. Earlier SIMPLNX code used an ID as an output index without rejecting negative values. DREAM.3D v6.5.171 also uses the signed ID in destination-pointer arithmetic. Its maximum-ID check does not reject negative IDs when the matrix size otherwise passes. SIMPLNX now scans every ID and returns `-81880` for a negative value before the copy starts. -### Note: All-negative FeatureIds — SIMPLNX improvement over SIMPL +**Affected users:** Users with negative IDs, including inputs that mix negative and nonnegative IDs. -Documented for completeness; not a deviation that disadvantages SIMPLNX. +**Recommendation:** Trust SIMPLNX. Correct negative IDs before execution. Use DREAM3D-NX v7.4.2 or later for the input check. -If all feature IDs are negative, `maxValue < 0` → SIMPLNX returns clean error -81880. In SIMPL, `largestFeature` stays 0 (negative values never satisfy `m_FeatureIds[i] > largestFeature`); `mismatchedFeatures` stays false; `copyCellData()` is called; inside, `featureIdx` is negative and `fPtr + (numComp * featureIdx)` is a pointer before the start of the allocation — undefined behavior. SIMPLNX is strictly safer for this input. +**Evidence:** AF-4 and AF-8 assert `-81880`. The release-source checks in `../provenance/CreateFeatureArrayFromElementArrayFilter.md` show the missing validation through v7.4.1 and the fix in v7.4.2. The legacy copy loop and size checks confirm the same unsafe negative-index path. --- -## Comparison method +### CreateFeatureArrayFromElementArrayFilter-D3 -| | | +| Field | Value | |---|---| -| **Comparison type** | Runtime A/B (both implementations executed on identical input) + static source analysis | -| **Fixture** | Synthetic 8×1×1 image geometry; `FeatureIds=[1,2,1,2,1,2,1,2]`; `CellFloat` (float32, 1-comp): `[10,20,30,20,10,20,30,20]`; `CellRGB` (uint8, 3-comp): `[[10,20,30],[40,50,60],[10,20,30],[40,50,60],[10,20,30],[40,50,60],[70,80,90],[40,50,60]]` | -| **Fixture coverage** | Exact Match case (`max(featureIds)+1 == AM.tupleCount`). The AM under-sized case cannot be generated from a SIMPL pipeline, so no A/B fixture for it exists. | -| **Tolerance** | Bit-identical (copy-only filter; no floating-point accumulation) | -| **Comparison driver** | `feature_from_element_vv/compare_outputs.py` | -| **Run date** | 2026-07-23 | -| **SIMPL runner** | `DREAM3D-6.5.171-Linux-x86_64/bin/PipelineRunner` | -| **NX runner** | `DREAM3DNX-Release-Linux-x64/Bin/nxrunner` | +| **Deviation ID** | `CreateFeatureArrayFromElementArrayFilter-D3` | +| **Filter UUID** | `50e1be47-b027-4f40-8f70-1283682ee3e7` | +| **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 element array longer than FeatureIds could cause a read beyond FeatureIds. + +**Root cause:** Bug. Both earlier implementations looped over the element tuple count and indexed FeatureIds without checking equal tuple counts. DREAM.3D v6.5.171 `dataCheck()` validates the selected arrays but does not compare their lengths. SIMPLNX preflight now rejects unequal counts with `-81883`. + +**Affected users:** Users who select element and ID arrays with different tuple counts, including arrays in separate attribute matrices. + +**Recommendation:** Trust SIMPLNX. Supply one FeatureId per element tuple. Use DREAM3D-NX v7.4.2 or later for the preflight check. + +**Evidence:** AF-9 supplies four element tuples and two IDs and asserts `-81883`. Release sources through v7.4.1 omit the check; v7.4.2 contains it. The legacy `dataCheck()` and `copyCellData()` confirm the unchecked length relationship. --- -## Results +## Part 2 — SIMPLNX defects and behavior changes + +### CreateFeatureArrayFromElementArrayFilter-D4 -| Test | SIMPL vs Oracle | NX vs Oracle | A/B | -|---|---|---|---| -| 1-component float32 (`CellFloat → FeatureFloat`) | PASS | PASS | MATCH | -| 3-component uint8 (`CellRGB → FeatureRGB`) | PASS | PASS | MATCH | +| Field | Value | +|---|---| +| **Deviation ID** | `CreateFeatureArrayFromElementArrayFilter-D4` | +| **Filter UUID** | `50e1be47-b027-4f40-8f70-1283682ee3e7` | +| **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:** Empty FeatureIds could cause an out-of-bounds read during maximum-ID lookup. + +**Root cause:** Bug. Earlier SIMPLNX code called `std::max_element` on an empty range, converted the returned end iterator to index zero, and read that index from the empty store. The invalid read caused the defect; the empty-range search itself was valid. The current algorithm returns `-81882` before scanning. DREAM.3D v6.5.171 uses a loop with a zero iteration count for empty IDs and has no maximum-element dereference. -**A/B result is complete for the relevant migration space.** All configurations that SIMPL can execute fall into the Exact Match or AM over-sized cases, where SIMPLNX output is bit-identical to SIMPL. D1 covers a configuration SIMPL could not produce output for, so no A/B comparison is possible or necessary for it. +**Affected users:** Users with empty element and FeatureIds arrays. + +**Recommendation:** Trust SIMPLNX. Supply nonempty input arrays. Use DREAM3D-NX v7.4.2 or later for the empty-input check. + +**Evidence:** AF-7 asserts successful preflight followed by error `-81882`. Release sources through v7.4.1 index the empty array; v7.4.2 has the guard. The legacy `execute()` source confirms that this maximum-lookup defect does not occur there. --- -## Migration recommendation +### CreateFeatureArrayFromElementArrayFilter-D5 + +| Field | Value | +|---|---| +| **Deviation ID** | `CreateFeatureArrayFromElementArrayFilter-D5` | +| **Filter UUID** | `50e1be47-b027-4f40-8f70-1283682ee3e7` | +| **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:** Resizing the feature matrix to the ID range could delete trailing tuples from existing child arrays. + +**Root cause:** Bug. Earlier SIMPLNX code always resized the output and feature matrix to the largest ID plus one. The current algorithm only grows the matrix. Before growth, it rejects a child array larger than the target with `-81881`. DREAM.3D v6.5.171 rejects a mismatched matrix size and does not resize its children. + +**Affected users:** Users with an oversized feature matrix, or a child array whose tuple count exceeds the proposed growth target. + +**Recommendation:** Trust SIMPLNX. Use DREAM3D-NX v7.4.2 or later to preserve existing feature tuples. Correct inconsistent child sizes before execution. -**No action required.** Any pipeline that ran successfully in SIMPL will produce bit-identical output in SIMPLNX. SIMPLNX additionally handles the AM under-sized case that SIMPL rejected with error -5555. +**Evidence:** AF-5 checks the child-array protection error. Release sources through v7.4.1 resize unconditionally; v7.4.2 contains the growth-only condition and child check. `vv_work:CreateFeatureArrayFromElementArrayFilter/notes/LegacyComparison.md` records the former 10-to-5 tuple truncation. No current fixture directly asserts preservation of an oversized matrix. 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. From 0780a7d2a442bee89c225a2a28dd5d9c8dd9a7d9 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 20:01:28 -0400 Subject: [PATCH 51/91] DOC: Rewrite the DBSCAN V&V record to follow the report template Signed-off-by: Michael Jackson --- src/Plugins/SimplnxCore/vv/DBSCANFilter.md | 62 +++---- .../SimplnxCore/vv/deviations/DBSCANFilter.md | 153 ++++++++---------- .../SimplnxCore/vv/provenance/dbscan_test.md | 111 +++++-------- 3 files changed, 132 insertions(+), 194 deletions(-) diff --git a/src/Plugins/SimplnxCore/vv/DBSCANFilter.md b/src/Plugins/SimplnxCore/vv/DBSCANFilter.md index c0ec59c58f..a466727924 100644 --- a/src/Plugins/SimplnxCore/vv/DBSCANFilter.md +++ b/src/Plugins/SimplnxCore/vv/DBSCANFilter.md @@ -17,23 +17,23 @@ | 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 test cases in `test/DBSCANTest.cpp`, plus 1 hidden OOC test. All 15 pass in CI at `65a3038e`. | -| Code paths enumerated | 20 of 23 exercised. The 3 uncovered paths are guarded by preflight or by a forest check, or have no isolating fixture. | +| 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. The six 2D label arrays are regression fixtures that the sklearn oracle confirms. The 3D label array has no external oracle. | -| Legacy comparison | Run against DREAM.3D v6.5.171 on the six 2D sklearn datasets. Four datasets match exactly. Two datasets differ as `DBSCAN-D1` predicts. | -| Bug flags| `DBSCAN-D4`. Open: `upgradeParametersImpl()` is not implemented, so SIMPLNX v1 pipelines lose a non-default parse order. | -| V&V phase| Complete. The OOC test suite was not run, because CI does not build the OOC configuration. | +| 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` puts points into clusters by density. It uses the GDCF algorithm (Boonchoo et al. 2019), which defines core objects on grid cells. A Class 2 scikit-learn oracle verifies the filter, with Class 1 fixtures and Class 4 invariants. Four of six reference datasets match exactly, two datasets differ as designed (`DBSCAN-D1`), and the V&V found and fixed four SIMPLNX defects. +`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 -*Evidence:* PR #1421 (commit `c1bc9114b7`, 2025-09-04) replaced the point-by-point DBSCAN from PR #994 with GDCF. +*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 @@ -41,24 +41,24 @@ *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:* `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. All registered tests pass in CI at `65a3038e`. +*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:* *Michael Jackson, 2026-08-28* ## Bugs found and fixed -This branch fixes all defects in this table. PR #1702 fixed `DBSCAN-D5`, `DBSCAN-D7` and `DBSCAN-D8`, and DREAM3D-NX v7.4.2 contains those fixes. PR #1766 fixed `DBSCAN-D6`, and the fix will be in the DREAM3D-NX release after v7.4.2. +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 `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. | -| `DBSCAN-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`). | -| `DBSCAN-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. | -| `DBSCAN-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. | +| 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 -20 of 23 paths exercised. Path 3 is not reachable because preflight accepts only `{2}` and `{3}` component shapes. Path 14 has no fixture that isolates it. Path 18 is not reachable because the filter skips labeling when no forest exists. +18 of 23 paths exercised by the registered tests. Paths 19 and 20 have a hidden OOC fixture, but the OOC suite was not run. Path 3 is not reachable because preflight accepts only `{2}` and `{3}` component shapes. Path 14 has no fixture that isolates it. Path 18 is not reachable because the filter skips labeling when no forest exists. Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/DBSCANDirect.cpp` (1273 lines) and `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/DBSCANScanline.cpp` (3643 lines). `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/DBSCAN.cpp` (34 lines) selects one of the two. @@ -84,8 +84,8 @@ The phases are (p) preflight, (a) grid construction, (b) core identification, (c | 16 | (e) Label | Root grid is not a core grid → label as noise (0) | `2D Test: Varied`, `2D Test: Aniso` | | 17 | (e) Label | Write the cluster ID of each grid to its points | All `2D Test: *` cases, `3D Test (LowDensityFirst)` | | 18 | (e) Label | `label()` runs with an empty forest → warning `-85640` | *Not directly tested. The filter does not call `label()` when `forestBuilt()` is false.* | -| 19 | (f) External | Input has a full 65,536-point batch and a partial tail batch | `genuine HDF5 65536-batch tail oracle` | -| 20 | (f) External | Adjacent core grids merge across a batch boundary | `genuine HDF5 65536-batch tail oracle` | +| 19 | (f) External | Input has a full 65,536-point batch and a partial tail batch | *Not directly tested. The hidden `genuine HDF5 65536-batch tail oracle` fixture requires OOC execution.* | +| 20 | (f) External | Adjacent core grids merge across a batch boundary | *Not directly tested. The hidden `genuine HDF5 65536-batch tail oracle` fixture requires OOC execution.* | | 21 | (p) Preflight | Mask is disabled → no cell-sized temporary mask | `No-mask preflight does not create a cell-sized temporary mask` | | 22 | (p) Preflight | Mask component shape is not `{1}` → error `-208` | `Reject multi-component mask in preflight` | | 23 | (p) Preflight | Mask tuple count is not equal to the input tuple count → error `-7587` | `Reject short mask in preflight`, `Reject short StringArray mask in preflight` | @@ -95,19 +95,19 @@ The phases are (p) preflight, (a) grid construction, (b) core identification, (c | Test case | Status | Notes | |-----------|--------|-------| | `SimplnxCore::DBSCAN: 2D Test: Aniso` | kept | Class 2 regression fixture. `LowDensityFirst` compares all labels with the exemplar. `SeededRandom` compares the cluster sizes. `Random` checks the Class 4 invariants only. Runs the Direct and Scanline algorithms. | -| `SimplnxCore::DBSCAN: 2D Test: Blobs` | kept | Same checks as `Aniso`. Matches scikit-learn exactly. | -| `SimplnxCore::DBSCAN: 2D Test: Noisy Circles` | kept | Same checks as `Aniso`. Matches scikit-learn exactly. | -| `SimplnxCore::DBSCAN: 2D Test: Noisy Moons` | kept | Same checks as `Aniso`. Matches scikit-learn exactly. | -| `SimplnxCore::DBSCAN: 2D Test: No Structure` | kept | Same checks as `Aniso`. Matches scikit-learn exactly. | -| `SimplnxCore::DBSCAN: 2D Test: Varied` | kept | Same checks as `Aniso`. Differs from scikit-learn as `DBSCAN-D1` predicts. | +| `SimplnxCore::DBSCAN: 2D Test: Blobs` | kept | Same checks as `Aniso`. The exemplar cluster counts and sorted sizes match scikit-learn. | +| `SimplnxCore::DBSCAN: 2D Test: Noisy Circles` | kept | Same checks as `Aniso`. The exemplar cluster counts and sorted sizes match scikit-learn. | +| `SimplnxCore::DBSCAN: 2D Test: Noisy Moons` | kept | Same checks as `Aniso`. The exemplar cluster counts and sorted sizes match scikit-learn. | +| `SimplnxCore::DBSCAN: 2D Test: No Structure` | kept | Same checks as `Aniso`. The exemplar cluster counts and sorted sizes match scikit-learn. | +| `SimplnxCore::DBSCAN: 2D Test: Varied` | kept | Same checks as `Aniso`. Differs from scikit-learn as `DBSCANFilter-D1` predicts. | | `SimplnxCore::DBSCAN: 3D Test (LowDensityFirst)` | kept | Compares all labels with the 3D exemplar and checks the AttributeMatrix tuple count. The exemplar has no external oracle. | | `SimplnxCore::DBSCAN: Analytical Fixture F1 - No Clusters Warning` | new-for-V&V | Class 1. Four corner points, epsilon 0.1, minimum points 5. Expects warning `-85640` and all IDs 0. | | `SimplnxCore::DBSCAN: Analytical Fixture F2 - Mask Exclusion` | new-for-V&V | Class 1. Three points, one masked, epsilon 1.0, minimum points 2. Expects IDs `[1, 1, 0]`. | -| `SimplnxCore::DBSCAN: Analytical Fixture F3 - All Points Masked` | new-for-V&V | Class 1. The F2 points with all points masked. Expects warning `-85640`, all IDs 0 and 1 AttributeMatrix tuple. Pins the `DBSCAN-D8` fix. | +| `SimplnxCore::DBSCAN: Analytical Fixture F3 - All Points Masked` | new-for-V&V | Class 1. The F2 points with all points masked. Expects warning `-85640`, all IDs 0 and 1 AttributeMatrix tuple. Pins the `DBSCANFilter-D8` fix. | | `SimplnxCore::DBSCAN: genuine HDF5 65536-batch tail oracle` | new-for-V&V | Hidden (`[.OocStoreContract]`); runs in the OOC build only. Class 1. Of 65,538 points, 5 are active. Four points form one cluster across the batch boundary, and all other labels are 0. | | `SimplnxCore::DBSCANFilter: No-mask preflight does not create a cell-sized temporary mask` | kept | Checks the preflight actions when the mask is disabled. | -| `SimplnxCore::DBSCAN: Reject multi-component mask in preflight` | new-for-V&V | Mask component shape `{2}`. Expects `-208` with the mask enabled, and no error with the mask disabled. Pins the `DBSCAN-D6` fix. | -| `SimplnxCore::DBSCAN: Reject short mask in preflight` | new-for-V&V | Four input tuples and three mask tuples. Expects `-7587` with the mask enabled, and no error with the mask disabled. Pins the `DBSCAN-D6` fix. | +| `SimplnxCore::DBSCAN: Reject multi-component mask in preflight` | new-for-V&V | Mask component shape `{2}`. Expects `-208` with the mask enabled, and no error with the mask disabled. Pins the `DBSCANFilter-D6` fix. | +| `SimplnxCore::DBSCAN: Reject short mask in preflight` | new-for-V&V | Four input tuples and three mask tuples. Expects `-7587` with the mask enabled, and no error with the mask disabled. Pins the `DBSCANFilter-D6` fix. | | `SimplnxCore::DBSCAN: Reject short StringArray mask in preflight` | new-for-V&V | A `StringArray` mask with a different tuple count. Expects a preflight error and no exception. | | `SimplnxCore::DBSCANFilter: SIMPL Backwards Compatibility` | kept | Converts the 6.4 and 6.5 SIMPL pipeline fixtures. Checks the parameters only. | @@ -119,10 +119,10 @@ The phases are (p) preflight, (a) grid construction, (b) core identification, (c ## Deviations from DREAM.3D v6.5.171 -See `vv/deviations/DBSCANFilter.md` for the root cause, affected users, and recommendation for each deviation. DREAM.3D v6.5.171 ran on the six 2D sklearn datasets in `dbscan_test.tar.gz`. +See `vv/deviations/DBSCANFilter.md` for the root cause, affected users, and recommendation for each deviation. The provenance sidecar records the comparison of DREAM.3D v6.5.171 with the saved 2D exemplar labels. A comparison with current execution output is required. | Deviation | Observed difference | |-----------|---------------------| -| `DBSCAN-D1` | Sparse data can give fewer clusters and more noise points, because a core object is a grid cell and not a point. | -| `DBSCAN-D2` | Cluster ID numbers can be different, because `LowDensityFirst` is the new default parse order. | -| `DBSCAN-D3` | The filter ignores the legacy `use_precaching` parameter. | +| `DBSCANFilter-D1` | Sparse data can give fewer clusters and more noise points, because a core object is a grid cell and not a point. | +| `DBSCANFilter-D2` | Cluster ID numbers can be different, because `LowDensityFirst` is the new default parse order. | +| `DBSCANFilter-D3` | The filter ignores the legacy `use_precaching` parameter. | diff --git a/src/Plugins/SimplnxCore/vv/deviations/DBSCANFilter.md b/src/Plugins/SimplnxCore/vv/deviations/DBSCANFilter.md index 716df27783..5247937e04 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/DBSCANFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/DBSCANFilter.md @@ -2,200 +2,175 @@ This file has two parts. Part 1 lists every documented behavioral difference between this SIMPLNX filter and its DREAM.3D v6.5.171 equivalent (D1–D3). Part 2 lists SIMPLNX defects and SIMPLNX behavior changes that are not differences from DREAM.3D v6.5.171 (D4–D8). -Entries are referenced by stable ID (`DBSCAN-D`) from the V&V report and from public migration guidance. The Filter UUID fields are the permanent cross-reference anchors. +Entries are referenced by stable ID (`DBSCANFilter-D`) from the V&V report and from public migration guidance. The Filter UUID is the permanent cross-reference anchor. --- ## Part 1 — Differences from DREAM.3D v6.5.171 -### DBSCAN-D1 +### DBSCANFilter-D1 | Field | Value | |---|---| -| **Deviation ID** | `DBSCAN-D1` | -| **SIMPLNX UUID** | `763dad44-fad7-4606-808f-617867257b98` | -| **Legacy SIMPL UUID** | `c2d4f1e8-2b04-5d82-b90f-2191e8f4262e` | +| **Deviation ID** | `DBSCANFilter-D1` | +| **Filter UUID** | `763dad44-fad7-4606-808f-617867257b98` | | **Status** | active | -**Symptom:** Sparse datasets where individual data points each have ≥ minPoints neighbors within ε (and would form clusters under traditional DBSCAN) may produce **more noise (cluster 0) points in SIMPLNX** if those neighborhoods span multiple grid cells with fewer than minPoints points per cell. +**Symptom:** SIMPLNX can label sparse points as noise (cluster 0) when traditional DBSCAN puts them in clusters. This occurs when a point neighborhood spans grid cells with fewer than the minimum number of points per cell. -**Evidence (DREAM.3D v6.5.171, 2026-10-20):** DREAM.3D v6.5.171, scikit-learn 1.7.1 and SIMPLNX ran on the six sklearn datasets in `dbscan_test.tar.gz`. The table gives the cluster count, the noise count and the sorted cluster sizes. An earlier run on a local proof version (2026-08-05) gave the same label for every point as DREAM.3D v6.5.171. Evidence: `dbscan_vv/ab_6_5_171_comparison_results.json`. +**Root cause:** Algorithmic choice. DREAM.3D uses the number of points within epsilon of a point to identify a core point. SIMPLNX uses GDCF (Boonchoo et al. 2019): a cell with side length epsilon divided by the square root of the dimension count is a core grid when its occupancy reaches the minimum point count. Adjacent cells below that count do not form a core grid, even when a point neighborhood has enough points. The rewrite preserves density-based clustering but changes the core criterion. -| Dataset | ε | minPts | DREAM.3D v6.5.171 | scikit-learn 1.7.1 | SIMPLNX | -|---|---|---|---|---|---| -| 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] | +**Affected users:** Users with sparse data, point spacing close to the grid-cell size, or a requirement for traditional point-level DBSCAN neighborhoods. -Four datasets match exactly in all three. For `ansio` and `varied`, DREAM.3D v6.5.171 and scikit-learn find the same number of clusters, and SIMPLNX finds 3. SIMPLNX does not find the micro-clusters of 6 points or fewer. Its large clusters match the large clusters of DREAM.3D v6.5.171 exactly for `varied` and within 6 points for `ansio`. +**Recommendation:** Trust SIMPLNX when grid-cell density is the required criterion. Lower the minimum point count or increase epsilon to increase grid occupancy. These changes do not make the two core criteria equivalent. -DREAM.3D v6.5.171 and scikit-learn differ by a small number of border points. DREAM.3D v6.5.171 uses `dist < epsilon`, and scikit-learn uses `dist <= epsilon`. The DREAM.3D v6.5.171 input is float32, and the scikit-learn input is float64. The `ansio` cluster of size 1 in DREAM.3D v6.5.171 comes from these differences. It does not change this deviation. - -**Root cause:** Algorithmic choice. SIMPLNX implements Grid-based DBSCAN (GDCF, Boonchoo et al. 2019). The core-object definition differs fundamentally: -- **Legacy (traditional DBSCAN)**: A data point `p` is a *core point* if the ε-ball centered on `p` contains ≥ minPoints data points (inclusive of `p` itself). Cluster membership is point-centric. -- **SIMPLNX (GDCF)**: A *core grid* is a grid cell (side length = ε/√dims) that contains ≥ minPoints data points. Cluster membership is grid-centric. - -Consequence: consider 4 data points, 2 per cluster, each with inter-point distance 0.5ε. In traditional DBSCAN with minPoints=2, each point's ε-ball contains its neighbor — both are core points and form a cluster. In SIMPLNX, if the 2 points per cluster happen to span two adjacent grid cells (each with 1 point), neither cell is a core grid, and both points are labeled noise (cluster 0), even though the traditional algorithm would cluster them. This occurs when point spacing is comparable to the grid cell side (ε/√dims). - -This deviation is not a bug — it is the intended behavior of GDCF, which sacrifices strict point-level equivalence for significant performance gains on large 3D datasets (O(n log n) vs. O(n²) distance checks). - -**Affected users:** Users who: -(1) Have data with very uniform spacing close to ε/√dims (exactly the scale where points are likely to straddle grid-cell boundaries), OR -(2) Use minPoints values that are sensitive to point-vs-grid counting (especially minPoints=2 or 3 on sparse data), OR -(3) Expect per-point ε-neighborhood semantics from the "minPoints" parameter description. - -Users with dense, well-clustered data (many points per grid cell) are unlikely to notice a difference. - -**Recommendation:** Trust SIMPLNX for production use on large 3D datasets. The GDCF algorithm provides substantial performance advantages. If strict traditional DBSCAN semantics are required, note that "minPoints" in SIMPLNX controls grid-cell occupancy rather than point-level ε-neighborhood density. Adjust minPoints and/or ε accordingly: lower minPoints (e.g., 2→1) or increase ε to ensure sufficient grid-cell occupancy. +**Evidence:** The saved comparison in `vv_work:DBSCANFilter/ab_6_5_171_comparison_results.json` uses DREAM.3D v6.5.171 output and SIMPLNX exemplar labels. Four datasets have equal cluster counts, noise counts and sorted sizes. For `ansio` and `varied`, SIMPLNX has 3 clusters; DREAM.3D v6.5.171 has 6 and 11, respectively. The complete table is in `../provenance/dbscan_test.md`. The script does not run the current SIMPLNX algorithm. --- -### DBSCAN-D2 +### DBSCANFilter-D2 | Field | Value | |---|---| -| **Deviation ID** | `DBSCAN-D2` | -| **SIMPLNX UUID** | `763dad44-fad7-4606-808f-617867257b98` | -| **Legacy SIMPL UUID** | `c2d4f1e8-2b04-5d82-b90f-2191e8f4262e` | +| **Deviation ID** | `DBSCANFilter-D2` | +| **Filter UUID** | `763dad44-fad7-4606-808f-617867257b98` | | **Status** | active | -**Symptom:** When run with default parameters on data where multiple clusters exist, SIMPLNX typically assigns cluster IDs in a different order than legacy DREAM.3D. The cluster membership (which points belong together) is functionally identical, but the numeric labels differ. - -**Root cause:** Algorithmic choice. SIMPLNX adds `LowDensityFirst` parse order (the new default) with no equivalent in legacy DREAM.3D. `LowDensityFirst` sorts core grids ascending by occupancy (less dense grids processed first) before the union-find merge phase, which changes the order in which cluster IDs are assigned. Legacy DREAM.3D used an effectively arbitrary (memory-layout-dependent) order for core point processing. - -The `Random` parse order in SIMPLNX uses a time-based seed (non-deterministic), so even successive identical runs may produce different cluster ID numbering. `SeededRandom` provides reproducibility with an explicit user seed. +**Symptom:** SIMPLNX can assign different cluster IDs from DREAM.3D when the input has multiple clusters. Numeric labels can differ even when membership is the same. The core-object difference in `DBSCANFilter-D1` can also change membership. -**Affected users:** Users who rely on specific cluster ID values (e.g., downstream filters that look for "cluster 1" by number rather than by properties). Cluster membership (which points are grouped together) is unaffected; only the numeric label assigned to each cluster changes. +**Root cause:** Algorithmic choice. SIMPLNX adds the default `LowDensityFirst` order, which processes core grids in ascending occupancy order. This changes the order in which the filter assigns cluster IDs. `Random` uses a time-based seed; `SeededRandom` uses the user seed. -**Recommendation:** Trust SIMPLNX. For deterministic output, use `LowDensityFirst` (default) or `SeededRandom`. Do not rely on specific cluster ID numbers in downstream pipelines; use cluster properties (size, centroid, etc.) for selection instead. +**Affected users:** Users whose downstream filters select a cluster by its numeric ID. +**Recommendation:** Trust SIMPLNX. Use `LowDensityFirst` or `SeededRandom` for repeatable results. Select clusters by properties such as size or centroid instead of their numeric IDs. --- -### DBSCAN-D3 +### DBSCANFilter-D3 | Field | Value | |---|---| -| **Deviation ID** | `DBSCAN-D3` | -| **SIMPLNX UUID** | `763dad44-fad7-4606-808f-617867257b98` | -| **Legacy SIMPL UUID** | `c2d4f1e8-2b04-5d82-b90f-2191e8f4262e` | +| **Deviation ID** | `DBSCANFilter-D3` | +| **Filter UUID** | `763dad44-fad7-4606-808f-617867257b98` | | **Status** | active | **Symptom:** SIMPL pipelines that explicitly set `use_precaching=true` or `use_precaching=false` will load in SIMPLNX without error, but the `use_precaching` value is silently ignored. -**Root cause:** Algorithmic choice. The legacy filter offered a memory/time trade-off switch: `use_precaching=true` pre-loaded data for faster neighbor queries at the cost of additional memory. The GDCF rewrite (PR #1421) replaced the distance-computation strategy entirely with a grid-based bit-packed adjacency table, making the trade-off concept obsolete. The parameter was dropped in `parametersVersion() == 2`. The `FromSIMPLJson` converter (see `DBSCANFilter.cpp`, starting at line 242) does not read `use_precaching` from SIMPL JSON, so old pipelines that set it will have it silently discarded. +**Root cause:** Algorithmic choice. DREAM3D-NX v7.2.2 replaced the distance-query strategy with a grid-based adjacency table and removed `use_precaching`. The filter no longer has this memory/time option. `DBSCANFilter::FromSIMPLJson()` does not read the parameter. -**Affected users:** Users converting SIMPL pipelines that explicitly set `use_precaching`. The converted pipeline will run correctly in SIMPLNX; only the parameter setting is lost. +**Affected users:** Users who convert pipelines that set `use_precaching`. The converted pipeline does not retain this setting. **Recommendation:** Trust SIMPLNX. No action required when converting pipelines — the parameter has no analog in SIMPLNX and its absence does not change the output. +**Evidence:** PR #1421 introduced the GDCF rewrite. `DBSCANFilter::parametersVersion()` records removal of `use_precaching`; `FromSIMPLJson()` does not read that parameter. + --- ## Part 2 — SIMPLNX defects and behavior changes -### DBSCAN-D4 +### DBSCANFilter-D4 | Field | Value | |---|---| -| **Deviation ID** | `DBSCAN-D4` | -| **SIMPLNX UUID** | `763dad44-fad7-4606-808f-617867257b98` | -| **Legacy SIMPL UUID** | `c2d4f1e8-2b04-5d82-b90f-2191e8f4262e` | +| **Deviation ID** | `DBSCANFilter-D4` | +| **Filter UUID** | `763dad44-fad7-4606-808f-617867257b98` | | **Status** | active | | **Affected released versions** | DREAM3D-NX v7.2.2 through v7.4.2. DREAM.3D v6.5.171 was not affected. | | **Fixed in** | Not fixed | -**Symptom:** SIMPLNX pipelines saved prior to PR #1421 that contain the parameter key `init_type_index` will silently lose their parse-order setting when loaded in SIMPLNX v2. The filter loads without error, but the parse order silently falls back to `LowDensityFirst` (the default). SIMPL (6.4/6.5) pipelines are **not** affected — SIMPL never had an `init_type_index` parameter. - -**Root cause:** Algorithmic choice (parameter rename as part of rewrite). During PR #1421, the parameter key was renamed from `init_type_index` to `parse_order_index`. `parametersVersion()` returns `2` and its comment block describes an intended v1→v2 migration that would read the old key and map it to the new key — but `upgradeParametersImpl()` is **not implemented** in `DBSCANFilter`. As a result, any early SIMPLNX pipeline JSON containing `init_type_index` silently deserializes as the default `LowDensityFirst` value. +**Symptom:** Pipelines saved in DREAM3D-NX v7.0.0 through v7.2.1 with the parameter key `init_type_index` lose their parse-order setting when loaded in DREAM3D-NX v7.2.2 or later. The filter loads without error, but the parse order silently falls back to `LowDensityFirst` (the default). DREAM.3D pipelines are not affected; the legacy filter had no `init_type_index` parameter. -The `FromSIMPLJson` converter (`DBSCANFilter.cpp`, starting at line 242) is not affected: SIMPL 6.4 and 6.5 never had an `init_type_index` parameter, confirmed by `test/simpl_conversion/6_4/DBSCANFilter.json` and `test/simpl_conversion/6_5/DBSCANFilter.json`. +**Root cause:** Bug. DREAM3D-NX v7.2.2 renamed `init_type_index` to `parse_order_index`. The filter has no `upgradeParametersImpl()` override to copy the old value, so it uses the default `LowDensityFirst`. The DREAM.3D converter is not affected because the legacy filter had no `init_type_index` parameter. -**Affected users:** SIMPLNX pipeline authors who explicitly saved a pipeline with a non-default parse order (`Random` or `SeededRandom`) using the v1 key `init_type_index` before PR #1421. These pipelines will silently default to `LowDensityFirst` without warning. Users who used the default `LowDensityFirst` or who created pipelines after PR #1421 are unaffected. +**Affected users:** SIMPLNX pipeline authors who explicitly saved a pipeline with a non-default parse order (`Random` or `SeededRandom`) using `init_type_index` in DREAM3D-NX v7.0.0 through v7.2.1. These pipelines will silently default to `LowDensityFirst` without warning. Users who used the default `LowDensityFirst` or who saved pipelines with `parse_order_index` in DREAM3D-NX v7.2.2 or later are unaffected. **Recommendation:** If you have early SIMPLNX pipelines that explicitly set a non-default parse order, verify the `parse_order_index` key is present in the pipeline JSON. Edit the pipeline to use `parse_order_index` if it still contains `init_type_index`. +**Evidence:** PR #1421 renamed the parameter. The local release sources `upstream/release/v7.2.1` and `upstream/release/v7.2.2` use `init_type_index` and `parse_order_index`, respectively. `DBSCANFilter` has no `upgradeParametersImpl()` override. The conversion fixtures in `test/simpl_conversion/6_4/DBSCANFilter.json` and `test/simpl_conversion/6_5/DBSCANFilter.json` have no `init_type_index` parameter. + --- -### DBSCAN-D5 +### DBSCANFilter-D5 | Field | Value | |---|---| -| **Deviation ID** | `DBSCAN-D5` | -| **SIMPLNX UUID** | `763dad44-fad7-4606-808f-617867257b98` | -| **Legacy SIMPL UUID** | `c2d4f1e8-2b04-5d82-b90f-2191e8f4262e` | +| **Deviation ID** | `DBSCANFilter-D5` | +| **Filter UUID** | `763dad44-fad7-4606-808f-617867257b98` | | **Status** | active | | **Affected released versions** | DREAM3D-NX v7.2.2 through v7.4.1. DREAM.3D v6.5.171 was not affected. | -| **Fixed in** | PR #1702, DREAM3D-NX v7.4.2 | +| **Fixed in** | DREAM3D-NX v7.4.2 | + +**Symptom:** DREAM3D-NX v7.4.2 changes the shuffle used by `SeededRandom` from DREAM3D-NX v7.2.2 through v7.4.1. The same seed can give different cluster IDs or different assignments of border points. -**Symptom:** For a given `SeededRandom` seed value, SIMPLNX now produces a different core-grid processing order — and therefore potentially different cluster-ID assignments — compared to earlier SIMPLNX builds (prior to this PR). Cluster *membership* (which points are grouped together) is unaffected for well-separated datasets; only cluster *ID numbering* and boundary-point assignment at cluster edges may differ. +**Root cause:** Bug. The previous shuffle excluded the last index as a swap partner and drew each partner from the full range instead of the remaining range. It also converted floating-point draws to indices. These choices biased the permutations. The filter now uses `std::shuffle`. -**Root cause:** The hand-rolled shuffle used in earlier builds had three correctness defects: (1) the last index was never selected as a swap partner because `maxIdx = size - 1` combined with a half-open `uniform_real_distribution` excluded it; (2) the draw used the full `[0, size-1]` range rather than `[0, i]`, introducing the classic naive-shuffle bias; (3) `uniform_real_distribution` + `std::floor` adds floating-point rounding bias absent from `uniform_int_distribution`. These defects made the shuffle subtly non-uniform. The replacement `std::shuffle` (C++ standard library Fisher-Yates) is correct and unbiased, but produces a different permutation for any given seed. +**Affected users:** Users who need to reproduce a cluster-ID assignment from DREAM3D-NX v7.2.2 through v7.4.1 with `SeededRandom`. `LowDensityFirst` does not use the shuffle. -**Affected users:** Users who relied on a specific `SeededRandom` seed to reproduce a particular cluster-ID assignment from a pre-PR build. The same seed now produces a correct unbiased shuffle instead of the previously biased one, so the resulting cluster-ID numbers will differ. Users running `LowDensityFirst` (the default) are entirely unaffected. Users running `Random` (time-based seed, non-deterministic by design) are unaffected in practice. +**Recommendation:** Trust SIMPLNX. Run the same seed in DREAM3D-NX v7.4.2 or later and use that output as the reference for repeatable results. -**Recommendation:** If reproducibility of specific cluster-ID values across builds is required, re-run with the same seed on the current build and use the new output as the reference. Cluster membership (grouping) is preserved for densely-clustered datasets; only the numeric label assigned to each cluster may change. +**Evidence:** PR #1702 replaced the shuffle. The source at `upstream/release/v7.4.2` calls `std::shuffle`; `upstream/release/v7.4.1` has the previous implementation. --- -### DBSCAN-D6 +### DBSCANFilter-D6 | Field | Value | |---|---| -| **Deviation ID** | `DBSCAN-D6` | -| **SIMPLNX UUID** | `763dad44-fad7-4606-808f-617867257b98` | -| **Legacy SIMPL UUID** | `c2d4f1e8-2b04-5d82-b90f-2191e8f4262e` | +| **Deviation ID** | `DBSCANFilter-D6` | +| **Filter UUID** | `763dad44-fad7-4606-808f-617867257b98` | | **Status** | active | | **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.2. | -| **Fixed in** | PR #1766, the DREAM3D-NX release after v7.4.2 | +| **Fixed in** | The DREAM3D-NX release after v7.4.2 | **Symptom:** With **Use Mask Array** enabled, SIMPLNX previously accepted a multi-component or short **Cell Mask Array** in preflight. The Direct path read the mask by flat element, while the external Scanline path rejected it during execution with `-54062`. Both paths now reject these inputs before execution: component shape other than `{1}` produces `-208`, and unequal tuple counts produce `-7587`. -**Root cause:** The mask selection parameter did not restrict component shape, and preflight did not compare the tuple counts of **Cell Mask Array** and **Attribute Array to Cluster**. Validation now applies before algorithm dispatch. This is a within-SIMPLNX correction; no new comparison with DREAM.3D v6.5.171 is claimed. +**Root cause:** Bug. The mask parameter did not restrict component shape, and preflight did not compare mask and input tuple counts. Validation now applies before algorithm selection. The evidence covers the SIMPLNX correction only. -**Affected users:** Users whose pipelines select a multi-component mask or a mask with a different number of tuples from the clustering DataArray. Previously accepted Direct executions may have used unintended mask elements or accessed beyond a short mask. +**Affected users:** Users who select a mask with multiple components or a tuple count different from the clustering array. The Direct algorithm could read unintended mask elements or access data beyond a short mask. -**Recommendation:** Select a boolean or uint8 mask DataArray with component shape `{1}` and one tuple per tuple in **Attribute Array to Cluster**. The preflight tuple-count error identifies both DataArrays and their tuple counts. Regression coverage: `SimplnxCore::DBSCAN: Reject multi-component mask in preflight` and `SimplnxCore::DBSCAN: Reject short mask in preflight` in `DBSCANTest.cpp`. +**Recommendation:** Select a boolean or uint8 mask DataArray with component shape `{1}` and one tuple per tuple in **Attribute Array to Cluster**. The preflight tuple-count error identifies both DataArrays and their tuple counts. + +**Evidence:** PR #1766 contains the mask correction. Commit `65a3038e` includes the component-shape and tuple-count checks and the `Reject multi-component mask in preflight`, `Reject short mask in preflight` and `Reject short StringArray mask in preflight` cases in `DBSCANTest.cpp`. The source at `upstream/release/v7.4.2` lacks these checks. This documentation checkout also lacks the correction and those three tests. --- -### DBSCAN-D7 +### DBSCANFilter-D7 | Field | Value | |---|---| -| **Deviation ID** | `DBSCAN-D7` | -| **SIMPLNX UUID** | `763dad44-fad7-4606-808f-617867257b98` | -| **Legacy SIMPL UUID** | `c2d4f1e8-2b04-5d82-b90f-2191e8f4262e` | +| **Deviation ID** | `DBSCANFilter-D7` | +| **Filter UUID** | `763dad44-fad7-4606-808f-617867257b98` | | **Status** | active | | **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | -| **Fixed in** | PR #1702, DREAM3D-NX v7.4.2 | +| **Fixed in** | DREAM3D-NX v7.4.2 | **Symptom:** In DREAM3D-NX v7.0.0 through v7.4.1, the `Random` parse order gave the same result on every run. It gave the same result as `SeededRandom` with the default seed. -**Root cause:** Bug. `DBSCANFilter::executeImpl` made a time-based seed and wrote it to the seed array. It then gave `k_SeedValue_Key` to the algorithm, not the generated seed. The GUI shows `k_SeedValue_Key` only for `SeededRandom`, so `Random` always used the default seed `5489`. PR #1702 gives the generated seed to the algorithm. +**Root cause:** Bug. `DBSCANFilter::executeImpl` made a time-based seed and wrote it to the seed array. It then gave `k_SeedValue_Key` to the algorithm, not the generated seed. The GUI shows `k_SeedValue_Key` only for `SeededRandom`, so `Random` always used the default seed `5489`. The filter now gives the generated seed to the algorithm. **Affected users:** Users who selected `Random` and expected a different core-grid order on each run. **Recommendation:** Trust SIMPLNX. The current `Random` parse order agrees with the documentation; use `SeededRandom` for repeatable results. +**Evidence:** PR #1702 corrected seed forwarding. The source at `upstream/release/v7.4.2` assigns `inputValues.Seed = seed`; `upstream/release/v7.4.1` forwards the user seed. + --- -### DBSCAN-D8 +### DBSCANFilter-D8 | Field | Value | |---|---| -| **Deviation ID** | `DBSCAN-D8` | -| **SIMPLNX UUID** | `763dad44-fad7-4606-808f-617867257b98` | -| **Legacy SIMPL UUID** | `c2d4f1e8-2b04-5d82-b90f-2191e8f4262e` | +| **Deviation ID** | `DBSCANFilter-D8` | +| **Filter UUID** | `763dad44-fad7-4606-808f-617867257b98` | | **Status** | active | | **Affected released versions** | DREAM3D-NX v7.2.2 through v7.4.1. DREAM.3D v6.5.171 was not affected. | -| **Fixed in** | PR #1702, DREAM3D-NX v7.4.2 | +| **Fixed in** | DREAM3D-NX v7.4.2 | **Symptom:** None observed. In DREAM3D-NX v7.2.2 through v7.4.1, a mask with no true values gave the correct warning `-85640`, but only by chance. -**Root cause:** Bug. When no point passed the mask, every grid bound stayed `quiet_NaN`. The algorithm then used `static_cast(NaN)` to calculate the grid dimensions, which is undefined behavior. arm64 gave 0 and x86-64 gave `INT64_MIN`; both gave an empty grid. PR #1702 adds an explicit check that returns warning `-85640` with a message about the mask. `Analytical Fixture F3 - All Points Masked` pins the result. +**Root cause:** Bug. When no point passed the mask, every grid bound stayed `quiet_NaN`. The algorithm then used `static_cast(NaN)` to calculate the grid dimensions, which is undefined behavior. The filter now has an explicit check that returns warning `-85640` with a message about the mask. **Affected users:** Users whose mask has no true values. No incorrect output was found. **Recommendation:** Trust SIMPLNX. The result no longer depends on undefined behavior. + +**Evidence:** PR #1702 added the empty-mask guard, which is present at `upstream/release/v7.4.2`. `SimplnxCore::DBSCAN: Analytical Fixture F3 - All Points Masked` checks warning `-85640`, zero labels and one AttributeMatrix tuple. diff --git a/src/Plugins/SimplnxCore/vv/provenance/dbscan_test.md b/src/Plugins/SimplnxCore/vv/provenance/dbscan_test.md index 416204d37d..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,55 +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 the DREAM.3D v6.5.171 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:** Michael Jackson - **Date:** 2026-08-28 -## 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 in the Class 2 oracle table above. - -This archive has **not** been regenerated. Regeneration is not needed because the independent oracle confirmed the existing arrays are correct. - -## DREAM.3D v6.5.171 comparison (2026-10-20) - -Script: `dbscan_vv/ab_6_5_171.py` -Results: `dbscan_vv/ab_6_5_171_comparison_results.json` - -The DREAM.3D v6.5.171 `PipelineRunner` ran the `DBSCAN` filter (DREAM3DReview plugin, UUID `c2d4f1e8`) on the same 6 sklearn datasets. An earlier run on a local proof version (2026-08-05) gave the same label for every point. Three-way comparison (DREAM.3D v6.5.171 vs. sklearn 1.7.1 vs. SIMPLNX): - -| Dataset | 6.5.171 n_clusters | sklearn n_clusters | SIMPLNX n_clusters | Match | -|---|---|---|---|---| -| ansio | 6 | 6 | 3 | 6.5.171 = 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 | 6.5.171 = 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 DREAM.3D v6.5.171 comparison. 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. From 37790d97636209728a866cccb526b8e82410c085 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 20:01:28 -0400 Subject: [PATCH 52/91] DOC: Rewrite the ErodeDilateBadData V&V record to follow the report template Signed-off-by: Michael Jackson --- .../vv/ErodeDilateBadDataFilter.md | 196 ++++++++---------- .../vv/deviations/ErodeDilateBadDataFilter.md | 100 ++++----- .../vv/provenance/6_6_erode_dilate_test.md | 20 +- 3 files changed, 144 insertions(+), 172 deletions(-) diff --git a/src/Plugins/SimplnxCore/vv/ErodeDilateBadDataFilter.md b/src/Plugins/SimplnxCore/vv/ErodeDilateBadDataFilter.md index 541e025c1d..c27cdee5a1 100644 --- a/src/Plugins/SimplnxCore/vv/ErodeDilateBadDataFilter.md +++ b/src/Plugins/SimplnxCore/vv/ErodeDilateBadDataFilter.md @@ -1,133 +1,118 @@ # V&V Report: ErodeDilateBadDataFilter -| | | -|-----------|------------------| -| Plugin | SimplnxCore | -| SIMPLNX UUID | `7f2f7378-580e-4337-8c04-a29e7883db0b` | -| SIMPLNX Human Name | Erode/Dilate Bad Data | -| DREAM3D 6.5.171 equivalent | `ErodeDilateBadData` — SIMPL UUID `3adfe077-c3c9-5cd0-ad74-cf5f8ff3d254` | -| Verified commit | ** | -| Status | COMPLETE | -| Sign-off | *Matthew Marine, 8/7/2026* second engineer: *Michael Jackson <mike.jackson@bluequartz.net>, 08-17-2026* | +| | | +|--------|--------------| +| Plugin | SimplnxCore | +| SIMPLNX UUID | `7f2f7378-580e-4337-8c04-a29e7883db0b` | +| SIMPLNX Human Name | Erode/Dilate Bad Data | +| DREAM.3D v6.5.171 equivalent | `ErodeDilateBadData` (Processing plugin, SIMPL UUID `3adfe077-c3c9-5cd0-ad74-cf5f8ff3d254`) | +| Verified commit | ** | +| Status | COMPLETE | +| Sign-off | Matthew Marine, 2026-08-07; second engineer: Michael Jackson, 2026-08-17 | ## At a glance -| Aspect | Current state | -|------------------------|--------------------------| -| Algorithm Relationship | **Port** — legacy `ErodeDilateBadData.{h,cpp}` was diffed line-by-line against `Algorithms/ErodeDilateBadData.cpp` this pass. Neighbor offsets, boundary-validity checks, vote/tie-break order, and transfer conditions are structurally identical. One divergence, since resolved — see Resolved Defects. | -| Oracle (confirmed) | **Class 2 (Reference implementation).** The 28 expected `FeatureIds`/`Misc` arrays (7 direction combinations × 2 operations × 2 iteration counts) are genuine DREAM3D 6.5.171 output, matched element-wise against SIMPLNX and compiled into `ErodeDilateBadDataTest.cpp` as constants so the comparison re-runs in CI without the legacy binary. Confirmed — `(Erode) Expanded` and `(Dilate) Expanded` pass, 28/28 combinations. | -| Code paths enumerated | **13 of 14 exercised.** All 6 face directions (-Z/-Y/-X/+X/+Y/+Z) confirmed hit by instrumentation. Only gap: the `m_ShouldCancel` early exits (Path 11) — no test injects a cancel signal. | -| Tests today | **10 TEST_CASEs:** 1 production-scale exemplar-archive comparison + 2 `GENERATE` parameter sweeps (14 valid runs each over direction × iteration count) + 1 ignored-path test + 3 preflight tests (1 warning, 2 errors) + 1 strict-placement test + 1 Cell Attribute Matrix alias test + 1 SIMPL backwards-compat. The 7 tests of the earlier V&V cycle passed in both in-core and OOC builds (2283 assertions, identical in each). | -| Exemplar archive | `6_6_erode_dilate_test.tar.gz` — provides `Input Data` plus legacy-generated `Exemplar Bad Data Erode` / `Exemplar Bad Data Dilate` containers on a 189×201×20 Small IN100 slice. Consumed by the `(Erode)` test here and shared with `ErodeDilateMaskTest` and `ErodeDilateCoordinationNumberTest`. SHA512 verified against `test/CMakeLists.txt`. | -| Legacy comparison | **Run.** Two independent comparisons: (1) all 28 parameter combinations run through DREAM3D 6.5.171 `PipelineRunner` against an HDF5 twin of the inline fixture and diffed element-wise — **28/28 exact matches** on both `FeatureIds` and `Misc`; (2) the `(Erode)` test compares SIMPLNX against a legacy-generated exemplar at production scale (759,780 cells, 6 cell arrays). | -| Bug flags | `ErodeDilateBadDataFilter-D1` (X/Y/Z direction parameters had no effect) — **confirmed and resolved.** One additional hypothesis (Dilate tie-break order) was investigated, found to be a false lead, and reverted — see deviations doc. | -| V&V phase | Oracle chosen and confirmed; legacy comparison run; direction-masking bug fixed; zero-dimensions preflight path now covered. Outstanding before promotion to COMPLETE: second-engineer sign-off, the uncovered cancel path (Path 11), and formalizing the manual 28-combination A/B run as an automated archive-based test (see deviations doc). | +| Aspect | Current state | +|--------|---------------| +| Algorithm Relationship | Port of `ErodeDilateBadData` in DREAM.3D v6.5.171. SIMPLNX uses rolling slice buffers for face-neighbor selection and tuple transfer. | +| Oracle (confirmed) | Class 4 checks preserve ignored arrays while FeatureIds change, including selection through an alias. Two fixtures encode these checks in `test/ErodeDilateBadDataTest.cpp`. | +| Code paths enumerated | 24 of 34 exercised. Cancellation, I/O failures, non-positive iterations and a partly zero geometry have no direct fixtures. | +| Tests today | 11 test cases: 10 ordinary cases cover comparisons, ignored arrays, validation and conversion; 1 hidden case generates inputs. | +| Exemplar archive | `6_6_erode_dilate_test.tar.gz` supplies inputs and stored outputs from a local proof version. The archive is a regression reference. | +| Legacy comparison | Not run on the current algorithm; the saved comparison has no recorded run date. | +| Bug flags | `ErodeDilateBadDataFilter-D2` | +| V&V phase | Complete. The non-positive iteration defect remains open. | ## Summary -`ErodeDilateBadDataFilter` erodes or dilates voxels with `FeatureId == 0` ("bad data") in an `ImageGeometry`, optionally restricted to any non-empty combination of X, Y, and Z face directions. Verification is **Class 2**: the 28 expected output arrays compiled into `(Erode) Expanded` / `(Dilate) Expanded` are genuine DREAM3D 6.5.171 output for the same fixture, matched element-wise across every operation × direction × iteration combination, and the `(Erode)` test additionally compares against a legacy-generated exemplar archive at production scale. One SIMPLNX-side bug was found and resolved (`ErodeDilateBadDataFilter-D1` — the direction parameters had no effect at all). The 7 tests of the earlier V&V cycle passed in both in-core and OOC builds with 2283 assertions; the suite now has 10 TEST_CASEs. +`ErodeDilateBadDataFilter` grows or shrinks cells with FeatureId zero along the selected image axes and copies the associated cell data. Class 4 checks verify ignored-array preservation, while 28 parameter combinations and an archive comparison check stored regression values. All pass in CI. ## Algorithm Relationship -*Classification:* **Port** ~~| Minor changes | Rewrite | New filter~~ +*Classification:* Port -*Evidence:* `SimplnxCoreLegacyUUIDMapping.hpp` maps legacy SIMPL UUID `3adfe077-c3c9-5cd0-ad74-cf5f8ff3d254` directly to `FilterTraits`, and `test/simpl_conversion/{6_4,6_5}/ErodeDilateBadDataFilter.json` carry the legacy `Direction`/`NumIterations`/`XDirOn`/`YDirOn`/`ZDirOn`/`FeatureIdsArrayPath`/`IgnoredDataArrayPaths` parameter set unchanged — the same filter with the same parameter model, not a reimplementation. Legacy source (`Source/Plugins/Processing/ProcessingFilters/ErodeDilateBadData.{h,cpp}`, from a sibling `DREAM3D` checkout on the authoring engineer's machine, not committed to this repository) was diffed line-by-line against `Algorithms/ErodeDilateBadData.cpp` this pass rather than inferred from documentation. - -*Port-time deltas:* - -1. **Face-neighbor offsets** — legacy computed `neighpoints[]` inline; SIMPLNX calls `initializeFaceNeighborOffsets(dims)` from `NeighborUtilities`. Same six offsets in the same `[-Z,-Y,-X,+X,+Y,+Z]` order; no output change. -2. **Boundary-validity checks** — legacy tested each face boundary with inline conditionals; SIMPLNX calls `computeValidFaceNeighbors(x, y, z, dims)`. Reproduces the same six conditions; no output change. -3. **Direction gating** — legacy ORs the direction flag into each per-face boundary check (`|| !m_ZDirOn`); SIMPLNX masks the per-voxel `isValidFaceNeighbor` array in `adjustValidNeighbors`. Equivalent once wired in — but it was *not* wired in, which is `ErodeDilateBadDataFilter-D1`. Now fixed and legacy-verified. -4. **Parallel array transfer** — legacy transferred all cell arrays in one serial pass interleaved with `FeatureIds`; SIMPLNX uses `ParallelTaskAlgorithm` for the non-`FeatureIds` arrays and transfers `FeatureIds` afterward, serially. Proven equivalent: erode only maps 0→>0 and dilate only >0→0, `neighbors[]` always points at a voxel whose relevant polarity is preserved, and each index is written at most once per pass, so no transfer predicate can observe a changed value. No output change. -5. **Cancel check** — SIMPLNX reads `m_ShouldCancel` once per Z-slice during the read-only Feature ID scan, and once more before the first morphology pass. The passes change `FeatureIds` and the sibling cell arrays in place, so after the first write they run to completion; a cancel never leaves the input partly modified. Legacy has no cancel check at all. Additive; no output change on a run to completion. -6. **Progress reporting** — SIMPLNX announces each phase with an info message and reports per-Z-slice progress through one `ThrottledMessageHandler`; legacy used `notifyStatusMessage`. No output change. - -*Material PRs since baseline:* four PRs have touched `Algorithms/ErodeDilateBadData.cpp` and account for the port-time deltas above: - -- **#1523** — factored the 6-face-neighbor code out into `NeighborUtilities` (deltas 1–2, and the `isValidFaceNeighbor` array that delta 3 masks). -- **#1590** — standardized 2D image handling (`VoxelNeighbors` specialization). -- **#1340** — thread-safe messaging rework (delta 6). -- **#1687** — this branch: fixes D1, adds the `-14601`/`-14602` preflight guards, and rebuilds the test suite. - -Earlier commits (#1249, #1017, #1013, #801) are compiler-warning, store-API, and rename churn with no behavioral content. - -*SIMPLNX implementation:* `Algorithms/ErodeDilateBadData.cpp` (231 lines) uses `NeighborUtilities::VoxelNeighbors` for face-neighbor offsets and boundary validity, and `ParallelTaskAlgorithm` to transfer non-`FeatureIds` arrays in parallel. - -## Resolved Defects - -### ErodeDilateBadDataFilter-D1: Direction parameters had no effect — fixed - -See deviations doc for full detail. Summary: `adjustValidNeighbors` was dead code (defined, never called); `XDirOn`/`YDirOn`/`ZDirOn` had zero effect on which face neighbors participated. Fixed by retyping the helper to mask the actual per-voxel validity array (`isValidFaceNeighbor`) with correct axis mapping, and calling it at `Algorithms/ErodeDilateBadData.cpp:162-163` for every bad-data voxel. Verified by regenerating all 28 exemplar constants from real DREAM3D 6.5.171 output (they were previously byte-identical across all 7 direction combinations for a given operation/iteration count) and matching them element-wise against SIMPLNX for every combination — see Oracle section. - -### Investigated, disproven, reverted: Dilate tie-break "fix" - -A plausible-looking bug hypothesis (last-bad-neighbor-wins vs. first-bad-neighbor-wins, for a good voxel with multiple bad neighbors) was implemented as a fix and then falsified by an actual legacy binary run. Reverted in full. Recorded as a confirmed non-deviation in the deviations doc so it isn't relitigated. This is why the Oracle section emphasizes binary-verified results over source-only reasoning: source comparison alone did not catch it, since legacy's own source has the identical "unconditional overwrite" line. Only running both binaries against the same input and diffing a value that is not blind to the tie-break (`Misc`, not `FeatureIds`) surfaced the truth. +*Evidence:* `src/Plugins/SimplnxCore/src/SimplnxCore/SimplnxCoreLegacyUUIDMapping.hpp` maps the legacy UUID to this filter; both implementations use six face neighbors, majority voting for erosion and the same face order. ## Oracle -*Class:* **2 (Reference implementation)** — expected values are genuine DREAM3D 6.5.171 output, at two different scales. +*Class:* 4 (Invariant). -*Applied:* Two oracles, both sourced from the legacy filter and neither derived from SIMPLNX output. +*Applied:* Require each ignored array to equal its input and require FeatureIds to change; also check that arrays retain their Attribute Matrix tuple dimensions. -- **Small-scale (28 combinations).** `CreateTestData()` builds an in-memory 4×4×2 (32-voxel) `ImageGeom` with a hand-authored `FeatureIds` array (features 1–6; bad voxels at flat indices 0, 10, 13, 14, 31) and a `Misc` `int32` array initialized so `Misc[i] == i`, making every copied tuple traceable to its source voxel by value alone. Pipeline JSONs (`DataContainerReader` → `ErodeDilateBadData` → `DataContainerWriter`) were run through the DREAM3D 6.5.171 `PipelineRunner` against an HDF5 twin of that fixture — verified byte-for-byte identical in dims, `FeatureIds`, and `Misc` before use — for all **28 combinations** of {Dilate, Erode} × {X, XY, XYZ, XZ, Y, YZ, Z} × {1, 2 iterations}. Outputs were diffed with `h5py` against SIMPLNX: **28/28 exact matches** on both arrays. Those legacy arrays are what the `k_ExemplarFeatureIds*` / `k_ExemplarData*` constants now hold — they were regenerated from the legacy binary, not hand-traced, which removes any question of their having been fitted to SIMPLNX's own behavior. -- **Production-scale (1 combination).** `6_6_erode_dilate_test.tar.gz` carries `Exemplar Bad Data Erode` and `Exemplar Bad Data Dilate` containers generated by the legacy SIMPL `ErodeDilateBadData` filter (UUID `3adfe077-…`, Erode and Dilate, `NumIterations=2`, all three directions on) on a 189×201×20 Small IN100 slice. The archive's embedded pipeline records that legacy build as `FilterVersion 6.6.338` — later than the 6.5.171 baseline — so this exemplar is genuine legacy output but is **not itself a 6.5.171 comparison**; the 6.5.171 comparison of record is the 28-combination run above. See `## Exemplar archive` and its provenance sidecar for both caveats. +*Encoded:* `src/Plugins/SimplnxCore/test/ErodeDilateBadDataTest.cpp::SimplnxCore::ErodeDilateBadDataFilter Ignored Path` and `SimplnxCore::ErodeDilateBadDataFilter: Ignored Array Cell AM Alias` — 2 fixtures. All pass in CI. -*Coverage split:* the archive exemplar was generated with all three directions enabled, so it cannot discriminate direction gating — a build with D1 still present passes it. That is exactly the gap the 28-combination sweep closes, and why both oracles are needed. Conversely the 32-voxel fixture is single-typed and single-component, so only the archive test exercises `copyTuple` across mixed types and component counts (`EulerAngles` float32×3, `IPFColor` uint8×3, `Mask` uint8, `Phases` int32, `Confidence Index`/`Image Quality` float32). +*Second-engineer review:* Michael Jackson, 2026-08-17. -*Encoded:* +## Bugs found and fixed -- `SimplnxCore::ErodeDilateBadDataFilter(Erode) Expanded` and `(Dilate) Expanded` — each `GENERATE`s `dirX,dirY,dirZ ∈ {true,false}` and `numIterations ∈ {1,2}`, reports the invalid all-directions-off combination with `SUCCEED`, and looks the expected arrays up in the 28-row `k_Exemplars` table. 14 valid parameterized runs each, both `FeatureIds` and `Misc` asserted, 1039 assertions each — all pass. -- `SimplnxCore::ErodeDilateBadDataFilter(Erode)` — `UnitTest::CompareExemplarToGeneratedData` against `6_6_erode_dilate_bad_data.dream3d`, 55 assertions. Passes. +All defects in this table are fixed. DREAM3D-NX v7.4.2 contains the fix for `ErodeDilateBadDataFilter-D1`. -*Second-engineer review:* **pending.** The previously open items — erode/dilate tie-break order, and whether direction combinations produce genuinely different output — were both resolved this pass by the legacy binary comparison above rather than by review alone. A named second-engineer sign-off on the oracle design is still required before Status can move to COMPLETE. +| Deviation | Defect | Affected released versions | Resolution | +|-----------|--------|----------------------------|------------| +| `ErodeDilateBadDataFilter-D1` | Disabled axes still contributed face neighbors to erosion and dilation. | DREAM3D-NX v7.4.1. DREAM.3D v6.5.171 was not affected. | The algorithm applies each direction flag to the two matching face neighbors. | ## Code path coverage -13 of 14 paths exercised. The algorithm has three logical phases: (a) preflight validation, (b) a per-voxel vote/mark scan over face neighbors, and (c) a per-array transfer pass. +24 of 34 paths exercised. Each uncovered row states the missing fixture. -Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ErodeDilateBadData.cpp` (231 lines). +Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ErodeDilateBadData.cpp` (310 lines), `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ErodeDilateBadDataFilter.cpp` (210 lines), `src/simplnx/Utilities/NeighborUtilities.hpp` (634 lines), and `src/simplnx/Utilities/SliceBufferedTransfer.hpp` (364 lines). + +Test names below omit the common `SimplnxCore::ErodeDilateBadDataFilter` prefix. The algorithm scans FeatureIds, selects source cells, then transfers completed slices. | # | Phase | Path | Test case | |---|-------|------|-----------| -| 1 | (a) Setup | `numFeatures` scan, face-offset/validity initialization, `adjustValidNeighbors` direction masking | All tests. Functional as of this pass — in the prior revision `adjustValidNeighbors` was dead code; it is now wired in and confirmed exercised for all 6 face directions (see below). | -| 2 | (b) Per-voxel | `featureName != 0` (good voxel) → skip | All tests (most of the 32 voxels are good) | -| 3 | (b) Per-voxel | `featureName == 0` + Dilate + neighbor `feature > 0` → `neighbors[neighborPoint] = voxelIndex` | `(Dilate) Expanded`, all 6 face directions confirmed hit | -| 4 | (b) Per-voxel | `featureName == 0` + Erode + neighbor `feature > 0` → vote accumulation, `neighbors[voxelIndex] = neighborPoint` on new max (ties keep the first-processed neighbor) | `(Erode) Expanded`, all 6 face directions confirmed hit | -| 5 | (b) Per-voxel | Erode post-vote cleanup — `featureCount[feature] = 0` for each valid neighbor of the bad voxel | `(Erode) Expanded`, all 6 face directions confirmed hit | -| 6 | (c) Transfer | `neighbor >= 0` + Erode condition (`featureName == 0 && featureIds[neighbor] > 0`) → `copyTuple` | `(Erode) Expanded`, `(Erode)` | -| 7 | (c) Transfer | `neighbor >= 0` + Dilate condition (`featureName > 0 && featureIds[neighbor] == 0`) → `copyTuple` | `(Dilate) Expanded` | -| 8 | (c) Transfer | `neighbor == -1` → skip (voxel untouched this iteration) | Both `Expanded` tests, implicitly (voxels far from bad data are unchanged in every expected array) | -| 9 | (a) Preflight | `!xDirOn && !yDirOn && !zDirOn` → error `-14601` (`k_NoDirectionsError`) | `No Direction` — asserts `invalid()` and `errors()[0].code == -14601` for both operations | -| 10 | (a) Preflight | `dims[0] == 0 \|\| dims[1] == 0 \|\| dims[2] == 0` → error `-14602` (`k_NoGeometryDimensionsError`) | `No Dimensions` — directions all **on** so the `-14601` check does not mask this path (the prior revision zeroed them and never reached here); asserts error code is exactly `-14602` | -| 11 | (b) Cancel | `m_ShouldCancel` read once per Z-slice in the read-only Feature ID scan, and once before the first morphology pass → early return with the input unchanged. Not read during the passes, which modify the input in place | *Not directly tested. Requires cancel-signal injection; no test sets `m_ShouldCancel` and asserts early termination.* Legacy has no cancel check at all, so SIMPLNX is ahead of legacy here — not a deviation. | -| 12 | (a) Preflight | Cell Feature Ids is not in the selected geometry's Cell Attribute Matrix → error `-14605` | `SimplnxCore::ErodeDilateBadDataFilter: Strict Feature Ids` — Feature Ids in another Attribute Matrix; asserts invalid preflight and exact error code `-14605`. | -| 13 | (a) Preflight | An Attribute Arrays to Ignore entry is not in the selected geometry's Cell Attribute Matrix → error `-14604` | `SimplnxCore::ErodeDilateBadDataFilter: Strict Feature Ids` — ignored array in another Attribute Matrix; asserts invalid preflight and exact error code `-14604`. | -| 14 | (c) Transfer | An ignored array selected through an alias of the Cell Attribute Matrix is excluded from modification | `SimplnxCore::ErodeDilateBadDataFilter: Ignored Array Cell AM Alias` — Cell Attribute Matrix reachable by two paths; asserts the ignored array remains unchanged. | - -**Per-direction coverage, confirmed by instrumentation:** `Algorithms/ErodeDilateBadData.cpp` was temporarily instrumented with per-face-direction hit counters at (a) the point immediately after the `isValidFaceNeighbor` gate in the vote/mark loop, (b) the point where the Dilate mark / Erode vote condition (`feature > 0`) fires, and (c) the equivalent point in the Erode cleanup loop. Running the full `(Erode) Expanded` + `(Dilate) Expanded` sweep produced non-zero counts for **every one of the 6 directions at all 3 measurement points** — vote/mark loop reached counts `-Z=38 -Y=111 -X=108 +X=106 +Y=64 +Z=97`; Dilate marking fired at `-Z=9 -Y=46 -X=37 +X=35 +Y=16 +Z=44`; Erode voting at `-Z=8 -Y=25 -X=24 +X=24 +Y=8 +Z=32`. The instrumentation was removed afterward; this records the empirical result, not a standing code artifact. - -Confirmed correct and deliberately not counted as deviations: - -- **Direction masking of the Erode `featureCount` reset loop.** SIMPLNX reuses the direction-masked `isValidFaceNeighbor` in the reset loop; legacy resets over *boundary-valid* neighbors and ignores the direction flags there. Simulating both variants across all 28 combinations gives identical output, and the equivalence is general: the reset set is a superset of the increment set in both variants, so `featureCount` returns to all-zeros after every bad voxel either way. -- **`neighbors` is intentionally not reset between iterations** — matches legacy. The deviations doc records the investigation that established this. -- **One progress throttle behind a mutex.** Every progress update goes through `sendThreadSafeProgressMessage()`, which takes `m_ProgressMessage_Mutex` before it calls the single `ThrottledMessageHandler`. +| 1 | Setup | Scan all slices to find the maximum FeatureId | `(Erode) Expanded`; `(Dilate) Expanded` | +| 2 | Scan | A nonzero FeatureId skips neighbor selection | Both `Expanded` cases compare unchanged cells | +| 3 | Scan | Dilate marks a good neighbor with the bad cell's source index | `(Dilate) Expanded` | +| 4 | Scan | Erode counts positive neighbor IDs and replaces the selected source only when its count exceeds the maximum | `(Erode) Expanded` | +| 5 | Scan | Erode resets counts for enabled, valid neighbors | `(Erode) Expanded` | +| 6 | Transfer | Copy the selected good-cell tuples into a bad cell | `(Erode) Expanded`; `(Erode)` | +| 7 | Transfer | Copy the selected bad-cell tuples into a good cell | `(Dilate) Expanded` | +| 8 | Transfer | A negative source mark preserves the destination tuple | Both `Expanded` cases compare all FeatureIds and Misc values | +| 9 | Preflight | All direction flags are false → error `-14601` | `No Direction` — both operations and both iteration counts | +| 10 | Preflight | All geometry dimensions are zero → error `-14602` | `No Dimensions` — direction flags stay enabled | +| 11 | Cancellation | Cancel during the initial FeatureId scan | *Not directly tested. Requires cancellation during a slice read.* | +| 12 | Preflight | FeatureIds are outside the selected Cell Attribute Matrix → error `-14605` | `: Strict Feature Ids` / `Feature Ids in another Attribute Matrix with the same tuple shape` | +| 13 | Preflight | An ignored array is outside the selected Cell Attribute Matrix → error `-14604` | `: Strict Feature Ids` / `Ignored array in another Attribute Matrix` | +| 14 | Selection | Resolve aliases to the selected Cell Attribute Matrix and exclude the ignored array | `: Ignored Array Cell AM Alias` | +| 15 | Scan | Enable or disable the two faces of each X, Y and Z axis | Both `Expanded` cases — seven non-empty axis combinations | +| 16 | Scan | Reject a face outside the image bounds | Both `Expanded` cases include boundary bad cells | +| 17 | Setup | Reset slice marks and read the previous output for another iteration | Both `Expanded` cases — two iterations | +| 18 | I/O | Load the first FeatureId slice for morphology | Both `Expanded` cases | +| 19 | I/O | Load a second slice when Z has more than one cell | Both `Expanded` cases | +| 20 | I/O | Rotate the window and read a later slice | `(Erode)` — the archive contains 20 slices | +| 21 | Transfer | Transfer the previous slice after all its marks are complete | Both `Expanded` cases | +| 22 | Transfer | Transfer the last slice of a multi-slice image | Both `Expanded` cases | +| 23 | Transfer | Transfer a single-slice image | `: Ignored Array Cell AM Alias` — FeatureIds change and the ignored array stays unchanged | +| 24 | Cancellation | Cancel immediately before the first morphology iteration | *Not directly tested. Requires cancellation before the first write.* | +| 25 | I/O | Propagate a read error from the initial maximum-ID scan | *Not directly tested. Requires an injected store read failure.* | +| 26 | I/O | Propagate failure while loading the first morphology slice | *Not directly tested. Requires an injected store read failure.* | +| 27 | I/O | Propagate failure while loading the second morphology slice | *Not directly tested. Requires an injected store read failure.* | +| 28 | I/O | Propagate failure while reading a slice after window rotation | *Not directly tested. Requires an injected store read failure.* | +| 29 | I/O | Propagate a tuple-transfer read or write failure for a previous slice | *Not directly tested. Requires an injected transfer failure.* | +| 30 | I/O | Propagate a tuple-transfer read or write failure for the final slice | *Not directly tested. Requires an injected transfer failure.* | +| 31 | Iteration | A non-positive count skips morphology | *Not directly tested. No fixture supplies zero or a negative count; see D2.* | +| 32 | Preflight | Emit warning `-14600` and the feature-data warning value | `: Preflight Feature Data Modification Warning` | +| 33 | I/O | Skip the next-slice read in a single-slice image | `: Ignored Array Cell AM Alias` | +| 34 | Preflight | One geometry dimension is zero while others are positive → error `-14602` | *Not directly tested. The existing fixture sets all three dimensions to zero.* | ## Test inventory +All ordinary cases are in `src/Plugins/SimplnxCore/test/ErodeDilateBadDataTest.cpp`. All pass in CI. The hidden generator is in `src/Plugins/SimplnxCore/test/ErodeDilateBadDataGeneratorTest.cpp`. + | Test case | Status | Notes | |-----------|--------|-------| -| `SimplnxCore::ErodeDilateBadDataFilter(Erode)` | kept | Class 2 production-scale oracle. `6_6_erode_dilate_test.tar.gz`, Erode, all directions on, 2 iterations, 189×201×20. `CompareExemplarToGeneratedData` over the 6 cell arrays present in both containers (`Confidence Index`, `EulerAngles`, `FeatureIds`, `Image Quality`, `Mask`, `Phases`; the exemplar's `IPFColor` has no counterpart in `Input Data` and is skipped); 55 assertions. The only test that exercises `copyTuple` across mixed types and component counts. Restored this pass after an earlier revision dropped it. | -| `SimplnxCore::ErodeDilateBadDataFilter(Erode) Expanded` | new-for-V&V | Class 2 small-scale oracle. `GENERATE` over 7 valid direction combinations × 2 iteration counts (14 runs); all-off reported via `SUCCEED`. Both `FeatureIds` and `Misc` asserted against the `k_Exemplars` table; 1039 assertions. Modified this pass: expected arrays regenerated from legacy output (previously byte-identical across direction combinations, per D1), the `Misc` assertion enabled for the first time, and the two duplicated dispatch helpers collapsed into one table lookup. | -| `SimplnxCore::ErodeDilateBadDataFilter(Dilate) Expanded` | new-for-V&V | Same sweep and same table, Dilate operation; 1039 assertions. Same modifications as the Erode sweep. | -| `SimplnxCore::ErodeDilateBadDataFilter Ignored Path` | new-for-V&V | Confirms an array listed in `IgnoredDataArrayPaths` (`Misc`) is left untouched, for both operations via `GENERATE(k_Dilate, k_Erode)`; 91 assertions. Modified this pass: the prior revision only preflighted, so comparing the DataStructure against a fresh fixture could not fail. It now executes the filter and additionally asserts that `FeatureIds` *did* change, which is what keeps the ignored-path check from passing vacuously. Verified by mutation — emptying the ignore list, or dropping the `execute()` call, each now fails the test. | -| `SimplnxCore::ErodeDilateBadDataFilter: Preflight Feature Data Modification Warning` | kept | Preflight test on the 32-voxel fixture: Dilate, 1 iteration, all directions on, no ignored arrays. Asserts a valid preflight, warning `-14600`, a `Feature Data Modification Warning` preflight value, and no `Stale Arrays` preflight value. Not an oracle test. | -| `SimplnxCore::ErodeDilateBadDataFilter No Direction` | new-for-V&V | Preflight-error test: all directions off, geometry otherwise valid, both operations × 2 iteration counts. Asserts `invalid()` and `errors()[0].code == -14601`; 25 assertions. Covers Path 9. | -| `SimplnxCore::ErodeDilateBadDataFilter No Dimensions` | new-for-V&V | Preflight-error test: `ImageGeom` dimensions forced to `{0,0,0}`, directions all on, Dilate. Asserts `invalid()` and `errors()[0].code == -14602`; 7 assertions. Covers Path 10. Modified this pass: previously it also zeroed all direction flags, which tripped `-14601` first and left Path 10 unreached. | -| `SimplnxCore::ErodeDilateBadDataFilter: SIMPL Backwards Compatibility` | kept | `DYNAMIC_SECTION` over `simpl_conversion/6_5/ErodeDilateBadDataFilter.json` (matched by `Filter_Uuid`) and `simpl_conversion/6_4/ErodeDilateBadDataFilter.json` (matched by `Filter_Name`; that fixture has no UUID field). Loads each legacy pipeline via `Pipeline::FromSIMPLFile`, confirms a single `PipelineFilter` with the right UUID, and checks the converted arguments: `Operation == k_Dilate`, `NumIterations == 5`, `XDirOn/YDirOn/ZDirOn == true`, geometry path `DataPath({"DataContainer"})`, feature-ids path `DataPath({"DataContainer","CellData","TestArray"})`; 27 assertions. `IgnoredDataArrayPaths` verified only by successful pipeline load, matching the pattern in `FillBadDataTest.cpp`. Not an oracle test. | -| `SimplnxCore::ErodeDilateBadDataFilter: Strict Feature Ids` | new-for-V&V | Three SECTIONs: correct Cell Attribute Matrix passes; Feature Ids in another Attribute Matrix returns `-14605`; an ignored array in another Attribute Matrix returns `-14604`. Checks exact error codes and diagnostic paths. Covers Paths 12–13. | -| `SimplnxCore::ErodeDilateBadDataFilter: Ignored Array Cell AM Alias` | new-for-V&V | Cell Attribute Matrix reachable by two paths. Selects the ignored array through the alias path, executes the filter, and checks that Feature Ids change while the ignored array remains unchanged. Covers Path 14. | - -The 7 tests of the earlier V&V cycle passed in both the in-core build (`NX-Com-Qt69-Vtk96-Rel`) and the out-of-core build (`simplnx-ooc-Rel`), with identical assertion counts in each — 2283 total (55 + 1039 + 1039 + 91 + 25 + 7 + 27). +| `SimplnxCore::ErodeDilateBadDataFilter(Erode)` | kept | Runs two iterations with all axes enabled. Compares matching cell arrays with `Exemplar Bad Data Erode` and checks tuple dimensions. | +| `SimplnxCore::ErodeDilateBadDataFilter(Erode) Expanded` | new-for-V&V | Compares all 32 FeatureIds and Misc values with stored constants for seven direction combinations and one or two iterations: 14 cases. | +| `SimplnxCore::ErodeDilateBadDataFilter(Dilate) Expanded` | new-for-V&V | Same 14-case sweep for dilation. Both sweeps skip all-directions-off; `No Direction` checks its rejection. | +| `SimplnxCore::ErodeDilateBadDataFilter Ignored Path` | new-for-V&V | Checks unchanged Misc and changed FeatureIds for both operations. This is a Class 4 invariant. | +| `SimplnxCore::ErodeDilateBadDataFilter: Preflight Feature Data Modification Warning` | kept | Checks valid preflight, warning `-14600`, the feature-data warning value and absence of a `Stale Arrays` value. | +| `SimplnxCore::ErodeDilateBadDataFilter No Direction` | new-for-V&V | Checks error `-14601` for both operations and one or two iterations with all axes disabled. | +| `SimplnxCore::ErodeDilateBadDataFilter No Dimensions` | new-for-V&V | Checks error `-14602` for dimensions `{0,0,0}` with all direction flags enabled. | +| `SimplnxCore::ErodeDilateBadDataFilter: Strict Feature Ids` | new-for-V&V | Three sections check valid placement and exact errors `-14605` and `-14604`, including diagnostic paths. | +| `SimplnxCore::ErodeDilateBadDataFilter: Ignored Array Cell AM Alias` | new-for-V&V | Selects FeatureIds and an ignored array through an alias. Checks modification actions, unchanged ignored values and changed FeatureIds. | +| `SimplnxCore::ErodeDilateBadDataFilter: SIMPL Backwards Compatibility` | kept | `SIMPL 6.5 (UUID)` and `SIMPL 6.4 (Filter_Name)` variants check the converted UUID, operation, iteration count, axes and paths. They do not execute morphology. | +| `SimplnxCore::ErodeDilateBadDataFilter: Generate Test Data` | kept | Hidden (`[.GenerateTestData]`). Writes 20×20×20 and 200×200×200 synthetic inputs; does not execute morphology. | ## Exemplar archive @@ -135,16 +120,11 @@ The 7 tests of the earlier V&V cycle passed in both the in-core build (`NX-Com-Q - **SHA512:** `5f0773e5d296936effbb2239965f5847e7c18533b0a2c3ec6a1d6a83b03417e5b459cce29808c8e0273613b3b6fa032c675e84926eb35d8da8a6ddc0641a0ef5` - **Provenance:** `src/Plugins/SimplnxCore/vv/provenance/6_6_erode_dilate_test.md` -The archive is shared: `6_6_erode_dilate_bad_data.dream3d` serves this filter, `6_6_erode_dilate_mask.dream3d` serves `ErodeDilateMaskTest`, and `6_6_erode_dilate_coordination_number.dream3d` serves `ErodeDilateCoordinationNumberTest`. Any regeneration must account for all three consumers. - -## Deviations from DREAM3D 6.5.171 - -No confirmed legacy deviations. The comparison was run at both scales described in the Oracle section — 28/28 exact matches on the 32-voxel fixture, and an element-wise match against the legacy-generated exemplar on the 189×201×20 Small IN100 slice. - -One SIMPLNX-side defect, since resolved: - -- `ErodeDilateBadDataFilter-D1` — the `XDirOn`/`YDirOn`/`ZDirOn` parameters had no effect on which face neighbors participated — see [`deviations/ErodeDilateBadDataFilter.md`](deviations/ErodeDilateBadDataFilter.md) +## Deviations from DREAM.3D v6.5.171 -One hypothesis was investigated and disproven (Dilate tie-break order: legacy matches SIMPLNX's original last-write-wins behavior), and is recorded in the same file as a confirmed non-deviation so it is not relitigated. -**Fixed in DREAM3D-NX 7.4.2:** `ErodeDilateBadDataFilter-D1`. +See `vv/deviations/ErodeDilateBadDataFilter.md` for the root cause, affected users and recommendation. This table lists only Part 1 entries. +| Deviation | Observed difference | +|-----------|---------------------| +| `ErodeDilateBadDataFilter-D2` | SIMPLNX accepts non-positive iteration counts and skips morphology. DREAM.3D v6.5.171 rejects them with `-5555`. | +| `ErodeDilateBadDataFilter-D3` | SIMPLNX rejects all-directions-off with `-14601`. DREAM.3D v6.5.171 accepts that setting and leaves the input unchanged. | diff --git a/src/Plugins/SimplnxCore/vv/deviations/ErodeDilateBadDataFilter.md b/src/Plugins/SimplnxCore/vv/deviations/ErodeDilateBadDataFilter.md index 52ed7b31f3..436b494d9b 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/ErodeDilateBadDataFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/ErodeDilateBadDataFilter.md @@ -1,91 +1,71 @@ -# Deviations from DREAM3D 6.5.171: ErodeDilateBadDataFilter +# Deviations from DREAM.3D v6.5.171: ErodeDilateBadDataFilter -This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent. - -Entries are referenced by stable ID (`ErodeDilateBadDataFilter-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. - -> **ID note:** `ErodeDilateBadDataFilter-D1` was cited as `ErodeDilateBadDataFilter-B1` in earlier revisions of this file and in the PR #1687 review thread, under a local `-B` convention for SIMPLNX-side bugs. That convention is used nowhere else in the repository, so the entry has been renumbered to the project-standard `-D` form (matching `CAxisSegmentFeaturesFilter-D1`, which documents an equivalent SIMPLNX-side bug found during its own V&V cycle). No external references to the old ID exist. - -## Headline: legacy A/B comparison performed — one SIMPLNX-side bug, fixed - -The gap recorded in the previous revision of this file ("no legacy comparison has been performed") is closed. A genuine DREAM3D 6.5.171 `PipelineRunner` was run against a hand-verified input twin of the C++ test fixture across **all 28 combinations** of operation (Erode/Dilate) × direction combination (7 valid combos) × iteration count (1, 2), and the legacy `ErodeDilateBadData.{h,cpp}` source was diffed line-by-line against the SIMPLNX algorithm. Both the binary and the source live in a sibling checkout on the authoring engineer's machine and are not committed to this repository. See `ErodeDilateBadDataFilter-D1` below and the V&V report's Oracle and Bug Fixes sections. +Part 1 lists current validation differences from DREAM.3D v6.5.171. Part 2 lists the fixed SIMPLNX direction-control defect. Entries share the stable `ErodeDilateBadDataFilter-D` sequence. --- -## ErodeDilateBadDataFilter-D1 +## Part 1 — Differences from DREAM.3D v6.5.171 + +### ErodeDilateBadDataFilter-D2 | Field | Value | |---|---| -| **Deviation ID** | `ErodeDilateBadDataFilter-D1` (formerly cited as `-B1`) | +| **Deviation ID** | `ErodeDilateBadDataFilter-D2` | | **Filter UUID** | `7f2f7378-580e-4337-8c04-a29e7883db0b` | -| **Status** | active in all releases through **7.4.1**; resolved in PR #1687 (2026-08-18) — resolved in DREAM3D-NX **7.4.2**| - -**Symptom:** In SIMPLNX releases prior to PR #1687, the *X Direction*, *Y Direction*, and *Z Direction* parameters had **no effect on the output**. They were parsed correctly from filter args into `ErodeDilateBadDataInputValues` (`ErodeDilateBadDataFilter.cpp:151-153`), but every face neighbor remained eligible — subject only to the geometry boundary — regardless of the flags. Disabling a direction silently produced the all-directions-on result. DREAM3D 6.5.171 honors the flags correctly, so any run with fewer than all three directions enabled diverges from legacy. This is also what produced the previous V&V pass's observation that "all 7 direction-combination fixtures encode byte-identical expected output": the fixture was not under-discriminating, the algorithm was ignoring direction entirely. +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | Not fixed | -**Root cause:** Bug (SIMPLNX). `adjustValidNeighbors` — the helper intended to mask face-neighbor validity by direction — was defined in `Algorithms/ErodeDilateBadData.cpp` but **never called** from `operator()()`. Confirmed by grepping the translation unit for `XDirOn` / `YDirOn` / `ZDirOn` / `adjustValidNeighbors(`: only the function definition matched, with no call site. Legacy achieves the same gating by ORing the direction flag into each per-face boundary check (`|| !m_ZDirOn` and siblings), so the legacy behavior was never in question. +**Symptom:** SIMPLNX accepts zero or negative iteration counts and leaves cell data unchanged. DREAM.3D v6.5.171 rejects these counts with error `-5555`. -*Branch-history note:* an earlier commit on the fix branch, `7e543f701` "Fixed XYZ direction off bug", *had* added a call to `adjustValidNeighbors`, but passed it the face-index-order array and bitwise-ANDed the index constants `0..5` against the direction booleans — which corrupts the iteration order rather than gating validity — and additionally gated `+X` by `zDir` and `+Z` by `xDir` (swapped axes). Since `2&1=0` and `3&1=1`, the iterated face list collapsed to `{-Z,-Y}` for *every* flag combination, including all-on. That call was later removed, leaving direction fully inert until it was reimplemented. +**Root cause:** Bug. `ErodeDilateBadDataFilter::preflightImpl()` has no positive-iteration check. `Int32Parameter` accepts these values, and the algorithm's iteration loop does not run when `NumIterations <= 0`. -**Fix:** `Algorithms/ErodeDilateBadData.cpp:64-80` — `adjustValidNeighbors` now takes the per-voxel `isValidFaceNeighbor` boolean array (the actual validity gate consumed by the vote/mark loop) and ANDs each of the six entries against the correct axis flag, using the named `VoxelNeighbors` constants rather than raw indices. It is called at `:162-163`, immediately after `computeValidFaceNeighbors`, for every bad-data voxel. +**Affected users:** Users whose scripts or saved pipelines supply an iteration count below one. -**Verification:** +**Recommendation:** Supply at least one iteration. Do not use successful preflight as proof that morphology will run with a non-positive count. -- All 28 `k_ExemplarFeatureIds*` / `k_ExemplarData*` constants in `ErodeDilateBadDataTest.cpp` were regenerated from genuine DREAM3D 6.5.171 binary output. They are legacy output, not a hand derivation — a Class 2 oracle. (They were previously byte-identical across all 7 direction combinations for a given operation and iteration count, which is what masked the bug.) -- 28/28 combinations (7 directions × 2 operations × 2 iteration counts) match legacy exactly, on both `FeatureIds` and the `Misc` tracer array — see the V&V report's Oracle section for the run details. -- `(Erode) Expanded` / `(Dilate) Expanded` (28 parameterized runs total, 2078 assertions combined) pass in both in-core and OOC builds. -- Per-direction structural coverage confirmed by temporary hit-count instrumentation — see "Per-direction code-path coverage" below. - -**Affected users:** Anyone who ran `ErodeDilateBadData` on any released version up to and including 7.4.1 with fewer than all three directions enabled. Their output silently matched the all-directions-on result, eroding or dilating across axes they had explicitly disabled. Users who left all three directions on (the default) are **unaffected** — that path was always correct, and the archive-based `(Erode)` regression test passes on affected builds for exactly that reason. - -**Recommendation:** Trust SIMPLNX at or after PR #1687, which agrees with 6.5.171 across all 28 parameter combinations. Results from affected builds that used a restricted direction set should be regenerated. +**Evidence:** Direct inspection of `ErodeDilateBadDataFilter.cpp` and `Algorithms/ErodeDilateBadData.cpp` in every release from `upstream/release/v7.0.0` through `upstream/release/v7.4.2` shows no positive-iteration guard. Current `src/simplnx/Parameters/NumberParameter.cpp::validateNumber()` returns success without a range check. DREAM.3D tag `v6.5.171`, `Source/Plugins/Processing/ProcessingFilters/ErodeDilateBadData.cpp::dataCheck()`, rejects non-positive counts. No current filter test supplies such a count. --- -## Non-deviations (confirmed correct — do not "fix") - -### Dilate tie-break: last-bad-neighbor-wins is correct, not a bug - -**Investigated and ruled out.** When a good voxel has two or more bad face-neighbors, `neighbors[neighborPoint] = voxelIndex` unconditionally overwrites on each bad neighbor visited, so whichever bad voxel is scanned *last* (highest flat index, in z/y/x order) wins. Since every bad voxel shares `FeatureId == 0`, this choice is invisible to a `FeatureIds`-only comparison — it shows up only in the `Misc` tracer array, which is why it was flagged as unverified in the prior pass and initially suspected as a bug in this one. - -A "first bad neighbor wins" fix (skip the overwrite if `neighbors[neighborPoint]` is already set, plus resetting `neighbors` to `-1` at the top of each iteration) was implemented and *appeared* correct until checked against real DREAM3D 6.5.171 output: `PipelineRunner` running Dilate / XYZ / 1 iteration against the matching legacy input produced `Misc` values matching the **original, unmodified** last-write-wins SIMPLNX behavior, not the "first-wins" rewrite (diverging at 3 of 32 indices: 9, 15, 30). The change was reverted in full — both the per-iteration reset and the overwrite guard. Unconditional last-write-wins, with `neighbors` initialized once before the iteration loop rather than per-iteration, is confirmed legacy-faithful. +### ErodeDilateBadDataFilter-D3 -This resolves the prior V&V pass's "second-engineer review pending: erode/dilate tie-break order" item — verified against actual legacy binary output, not source reading alone. - -### Erode tie-break: first-processed-neighbor-wins is correct - -Vote-count-based, using `[-Z,-Y,-X,+X,+Y,+Z]` scan order; a later neighbor's vote must strictly exceed the current maximum to replace the leader. Matches legacy source line-for-line (identical vote and comparison logic) and matches legacy binary output for all 28 tested combinations. Not a deviation. - -### Direction masking of the Erode `featureCount` reset loop - -SIMPLNX reuses the direction-masked `isValidFaceNeighbor` array in the post-vote reset loop; legacy resets over *boundary-valid* neighbors and ignores the direction flags there. Simulating both variants across all 28 combinations gives identical output, and the equivalence is general rather than incidental: the reset set is a superset of the increment set in both variants, so `featureCount` returns to all-zeros after every bad voxel either way. Not a deviation. +| Field | Value | +|---|---| +| **Deviation ID** | `ErodeDilateBadDataFilter-D3` | +| **Filter UUID** | `7f2f7378-580e-4337-8c04-a29e7883db0b` | +| **Status** | active | -### Deferring the FeatureIds transfer to a second pass +**Symptom:** SIMPLNX rejects a pipeline with all three direction flags disabled. DREAM.3D v6.5.171 accepts it and leaves the cell arrays unchanged. -Legacy interleaves the `FeatureIds` transfer with the other cell arrays in a single pass, mutating `m_FeatureIds` as it goes. SIMPLNX transfers the other arrays first (in parallel) and `FeatureIds` afterward, serially. Equivalent: erode only maps 0→>0 and dilate only >0→0, `neighbors[]` always points at a voxel whose relevant polarity is preserved by the operation, and each index is written at most once per pass — so neither transfer predicate can ever observe a changed value. Not a deviation. +**Root cause:** Algorithmic choice. SIMPLNX requires at least one enabled axis and returns `-14601` before execution. DREAM.3D v6.5.171 has no equivalent preflight guard; its six direction checks exclude every neighbor. -### Legacy tie-break language says "chosen randomly"; SIMPLNX is deterministic +**Affected users:** Users who disable all axes to bypass morphology. DREAM3D-NX v7.4.2 and later require an enabled axis. -The SIMPLNX filter markdown (`docs/ErodeDilateBadDataFilter.md`), carried over from legacy documentation, stated that erode ties are broken "randomly." Both the legacy *source* (`ErodeDilateBadData.cpp`, `Source/Plugins/Processing/ProcessingFilters/`) and the legacy *binary* output are fully deterministic — the same first-processed-wins scan order as SIMPLNX, with no RNG anywhere in the algorithm. "Randomly" is inaccurate documentation language, not a behavioral characteristic; SIMPLNX's determinism is not a deviation. +**Recommendation:** Trust SIMPLNX validation. Disable the filter in the pipeline when morphology is not required. -**The user-facing doc has been corrected** to describe the actual behavior: the six face neighbors are visited in the fixed order `[-Z,-Y,-X,+X,+Y,+Z]` and a later neighbor must have a strictly greater count to displace the leader, so the earliest tied neighbor in that scan order wins. The same edit documents the two preflight errors (`-14601` no directions enabled, `-14602` zero-length geometry dimension) and the effect of the direction restrictions. +**Evidence:** The `No Direction` test checks `-14601` for both operations and one or two iterations. The guard is present in `upstream/release/v7.4.2:src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ErodeDilateBadDataFilter.cpp`. The DREAM.3D `v6.5.171` source has direction checks in the neighbor loop and no all-directions-off rejection. -### Cancel check present in SIMPLNX, absent in legacy +--- -`operator()` reads `m_ShouldCancel` once per Z-slice inside the iteration loop and returns immediately if set. Legacy's equivalent loop has no cancel check at all. Additive capability with no effect on a run to completion — SIMPLNX is ahead of legacy here, not behind. Not a deviation. (This path is Path 11 in the V&V report's coverage table and is the one enumerated path no test exercises.) +## Part 2 — SIMPLNX defects and behavior changes -## Per-direction code-path coverage +### ErodeDilateBadDataFilter-D1 -Previously an open question ("could not distinguish correct per-direction gating from a no-op gate" — previously unresolved). Now resolved by two independent means: +| Field | Value | +|---|---| +| **Deviation ID** | `ErodeDilateBadDataFilter-D1` | +| **Filter UUID** | `7f2f7378-580e-4337-8c04-a29e7883db0b` | +| **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 | -1. **Behavioral:** expected data now differs by direction combination (see D1 above) and matches legacy per-combination, 28/28. -2. **Structural:** `Algorithms/ErodeDilateBadData.cpp` was temporarily instrumented with per-face-direction (`-Z/-Y/-X/+X/+Y/+Z`) hit counters and run through the full `(Erode) Expanded` + `(Dilate) Expanded` sweep. All six directions were hit, both at the "loop reached" level and at the "vote/mark condition fired" level, in both the vote/mark loop and the Erode-only cleanup loop. Instrumentation was removed afterward and is not shipped. +**Symptom:** Disabling X, Y or Z still allowed erosion or dilation along that axis. -## What would need to happen to extend this further +**Root cause:** Bug. The neighbor-utility refactor omitted direction gating from the scan. The filter passed the flags to the algorithm, but the algorithm did not use them. Current `Algorithms/ErodeDilateBadData.cpp` calls `adjustValidNeighbors()` after each boundary check; the helper applies each flag to the two matching faces. -Everything listed in the prior revision of this file has been done: legacy binary obtained and run, SIMPLNX output compared element-wise against legacy output, and the neighbor-selection and tie-break logic diffed directly against legacy source. The zero-dimensions preflight path (`-14602`), also previously listed here as uncovered, is now reached and asserted by the `No Dimensions` test. +**Affected users:** Users of DREAM3D-NX v7.4.1 who disable an axis that has valid neighbors. Runs with all axes enabled do not expose this defect. -Remaining follow-up (not gating — see the V&V report's V&V phase row): +**Recommendation:** Trust SIMPLNX in DREAM3D-NX v7.4.2 or later. Regenerate results produced with a restricted direction set in v7.4.1. -1. **Automate the 28-combination A/B run.** It was a manual, one-time verification (pipeline JSONs through `PipelineRunner`, output diffed with `h5py`), not wired into CI. The expected values are now compiled into the test as constants, so the *comparison* does re-run in CI — but a future change to the fixture would require redoing the legacy run by hand. Consider checking in the legacy `.dream3d` input/output pairs as an exemplar archive with a provenance sidecar, matching the pattern used by `FillBadDataFilter`'s `FillBadData_SmallIN100` test. -2. **Cover the cancel path (Path 11).** Requires cancel-signal injection; no test currently sets `m_ShouldCancel` and asserts early termination. -3. **Add a production-scale Dilate test.** `6_6_erode_dilate_test.tar.gz` already contains an unused `Exemplar Bad Data Dilate` container, so this costs no new test data. See `../provenance/6_6_erode_dilate_test.md`. +**Evidence:** The algorithm in `upstream/release/v7.0.0` through `upstream/release/v7.4.0` gates all six faces. The v7.4.1 source has no direction checks. The v7.4.2 source calls a helper that gates the boolean validity array with the correct axis mapping. DREAM.3D tag `v6.5.171`, `Source/Plugins/Processing/ProcessingFilters/ErodeDilateBadData.cpp`, gates all six faces. PR #1687 (`ea4f1ae20`) supplies the correction. The two `Expanded` tests compare FeatureIds and Misc for all seven direction combinations and one or two iterations. The saved intermediate-fix review is `vv_work:ErodeDilateBadDataFilter/pr_comment.md`; it is separate from current CI results. diff --git a/src/Plugins/SimplnxCore/vv/provenance/6_6_erode_dilate_test.md b/src/Plugins/SimplnxCore/vv/provenance/6_6_erode_dilate_test.md index a4137017d2..284288487e 100644 --- a/src/Plugins/SimplnxCore/vv/provenance/6_6_erode_dilate_test.md +++ b/src/Plugins/SimplnxCore/vv/provenance/6_6_erode_dilate_test.md @@ -1,6 +1,6 @@ # Exemplar Archive Provenance: 6_6_erode_dilate_test.tar.gz -This sidecar records the shared Erode/Dilate exemplar archive and the ErodeDilateMaskFilter comparison evidence. +This sidecar records the shared Erode/Dilate exemplar archive and the mask and bad-data comparison evidence. --- @@ -15,7 +15,7 @@ This sidecar records the shared Erode/Dilate exemplar archive and the ErodeDilat | **Generated on** | 2023-02-07 to 2023-02-15 (inferred from PR #439) | | **Generated at commit** | `98d2681e7` (PR #439 merge, inferred) | -Read-only inspection on 2026-10-20 confirmed that the archive SHA512 matches `src/Plugins/SimplnxCore/test/CMakeLists.txt:234` and the value above. +The SHA512 above matches the archive registration in `src/Plugins/SimplnxCore/test/CMakeLists.txt`. The first registration is `98d2681e7`, authored by Michael Jackson, with PR #439 in its subject. ## How it was generated @@ -45,12 +45,16 @@ The archive supplies regression references. The independent correctness oracle f Both mask exemplar tests run two iterations with all axes enabled, compare the cell arrays with the matching exemplar, and check tuple dimensions. These settings cannot detect ignored direction flags. The archive is separate from the stock DREAM.3D v6.5.171 comparison below. +The bad-data `(Erode)` test compares matching cell arrays with `Exemplar Bad Data Erode`. Its two `Expanded` tests instead use 28 pairs of literal FeatureIds and Misc arrays from `ErodeDilateBadDataTest.cpp`. These are regression references. The Class 4 checks in `Ignored Path` and `Ignored Array Cell AM Alias` require ignored values to stay unchanged while FeatureIds change. + ## Oracle provenance (Classes 2, 3, 5 only) ErodeDilateMaskFilter uses Class 1, so no external correctness-oracle provenance block is required. The following artifacts document the separate legacy comparison. Run against DREAM.3D v6.5.171 on 2026-08-18: 28 of 28 mask cases match. `vv_work:ErodeDilateMaskFilter/ReadMe.md` identifies the stock `PipelineRunner` and records the run date. `vv_work:ErodeDilateMaskFilter/results_compare.txt` gives the 28 element-wise matches from `vv_work:ErodeDilateMaskFilter/compare.py`. +The recorded mask comparison predates `4ec21e93f` (2026-09-20, rolling slice buffers) and `ba246bc7c` (2026-09-24, shared direction helper). It does not verify the current algorithm. A new comparison is required. + The comparison uses a 5×5×1 centre seed and a 3×3×3 hollow cube. Each fixture runs dilation and erosion for one iteration on each of seven non-empty axis combinations. The Z-only cases on the single-slice fixture preserve the input; the cube supplies the Z-direction evidence. `vv_work:ErodeDilateMaskFilter/results_oracle_check.txt` confirms that both binaries match five hand-derived masks from `vv_work:ErodeDilateMaskFilter/oracle_check.py`: @@ -65,13 +69,21 @@ The comparison uses a 5×5×1 centre seed and a 3×3×3 hollow cube. Each fixtur The zero-iteration probe in `vv_work:ErodeDilateMaskFilter/ReadMe.md` records DREAM.3D v6.5.171 rejecting the input with `-5555` while SIMPLNX returned the unchanged mask. Its inputs are `vv_work:ErodeDilateMaskFilter/extra/legacy_numiter0.json` and `vv_work:ErodeDilateMaskFilter/extra/nx_numiter0.d3dpipeline`. `ErodeDilateMaskFilter-D1` documents the correction to this validation difference. -The current test source is identical to `01809c236`. All registered ErodeDilateMaskFilter tests pass in CI at `01809c236` on Linux, macOS and Windows. +The ordinary mask and bad-data tests run in Linux, macOS and Windows in-core builds. All pass in CI at `419f9db35`. The bad-data test source is identical to that revision; its hidden input generator is separate from the ordinary regression cases. + +The bad-data artifacts are `vv_work:ErodeDilateBadDataFilter/ab/run_ab.py`, `vv_work:ErodeDilateBadDataFilter/oracle.py` and `vv_work:ErodeDilateBadDataFilter/pr_comment.md`. The runner script names the stock DREAM.3D v6.5.171 `PipelineRunner`. The review note records 0 of 28 matches for an intermediate implementation that masked face indices, and 28 of 28 matches between DREAM.3D and the corrected Python simulation. The simulation explicitly reproduces the implementation structure; it is diagnostic evidence, not an independent morphology oracle. + +The saved bad-data folder contains no ReadMe, dated result file or result from the current algorithm. The earlier report at `ea4f1ae20` (2026-08-18) states 28 of 28 matches after the fix but gives no comparison date. Later algorithm changes are `4ec21e93f` (2026-09-20, rolling slice buffers), `ba246bc7c` (2026-09-24, shared direction helper) and `a33c370ca` (2026-09-25, messaging API). Thus the earlier comparison does not describe execution of the current algorithm. + +Source comparison preserves useful tie-breaking evidence: dilation overwrites a destination mark when another bad cell reaches it; erosion changes its selected neighbor only when the count strictly exceeds the maximum. Both visit faces in `[-Z,-Y,-X,+X,+Y,+Z]` order. Current slice marks reset each iteration, and `SliceBufferedTransferOneZ` transfers completed slices; the earlier full-volume transfer description does not apply. ## Second-engineer oracle review -- **Reviewer:** Nathan Young, as the approving reviewer of PR #1717 +- **Reviewer:** Nathan Young for ErodeDilateMaskFilter, as the approving reviewer of PR #1717 - **Date:** 2026-08-25 +ErodeDilateBadDataFilter records Michael Jackson, 2026-08-17, as the second engineer in its report sign-off. Its prior Oracle field says review is pending and supplies no separate oracle-design approval. + ## Regenerated to fix a circular-oracle situation? No. The embedded pipelines identify the local proof version as the producer of the stored exemplars. The archive is retained unchanged. The hand-derived mask expectations in the unit tests remain the independent correctness oracle. From dd4c3c4bfd37bf55d53ce281c3ad9a5cee3004a0 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 20:01:28 -0400 Subject: [PATCH 53/91] DOC: Rewrite the ErodeDilateMask V&V record to follow the report template Signed-off-by: Michael Jackson --- .../SimplnxCore/vv/ErodeDilateMaskFilter.md | 20 +++++++------- .../vv/deviations/ErodeDilateMaskFilter.md | 26 +++++++------------ 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/src/Plugins/SimplnxCore/vv/ErodeDilateMaskFilter.md b/src/Plugins/SimplnxCore/vv/ErodeDilateMaskFilter.md index e7b5c59825..c8423d4d0f 100644 --- a/src/Plugins/SimplnxCore/vv/ErodeDilateMaskFilter.md +++ b/src/Plugins/SimplnxCore/vv/ErodeDilateMaskFilter.md @@ -17,23 +17,23 @@ | Aspect | Current state | |--------|---------------| | Algorithm Relationship | Port of `ErodeDilateMask` in DREAM.3D v6.5.171. SIMPLNX uses rolling slice buffers for the same synchronous face-neighbour operations. | -| Oracle (confirmed) | Class 1 analytical masks are encoded in 8 test cases in `test/ErodeDilateMaskTest.cpp`. All pass in CI at `01809c236`. | +| Oracle (confirmed) | Class 1 analytical masks are encoded in 8 test cases in `test/ErodeDilateMaskTest.cpp`. All pass in CI. | | Code paths enumerated | 18 of 26 exercised by the mapped test cases. All-directions-off, six I/O failure paths and external-store execution lack direct tests in this file. | | Tests today | 13 test cases: 8 analytical, 2 exemplar comparisons, 1 validation, 1 SIMPL conversion and 1 hidden data generator. | | Exemplar archive | `6_6_erode_dilate_test.tar.gz` supplies inputs and expected outputs for the two exemplar comparisons. It does not establish parity with DREAM.3D v6.5.171. | -| Legacy comparison | Run against DREAM.3D v6.5.171 on 2026-08-18: 28 of 28 mask cases match on a 5×5×1 centre seed and a 3×3×3 hollow cube. | +| Legacy comparison | Not run for the current algorithm. The recorded comparison with DREAM.3D v6.5.171 predates the change to rolling slice buffers. | | Bug flags | None. | -| V&V phase | Analytical fixtures and regressions for both fixed defects are encoded. All registered tests pass in CI at `01809c236`. | +| V&V phase | Analytical fixtures and regressions for both fixed defects are encoded. All pass in CI. A legacy comparison with the current algorithm remains to be done. | ## Summary -`ErodeDilateMaskFilter` grows or shrinks a boolean cell mask by one face-neighbour layer per iteration on the selected axes. Class 1 fixtures check exact masks, and two archive tests compare reconstruction data with stored exemplars. SIMPLNX fixes ignored direction flags and acceptance of non-positive iteration counts. All registered tests pass in CI at `01809c236`, and all 28 legacy comparison cases match. +`ErodeDilateMaskFilter` grows or shrinks a boolean cell mask by one face-neighbour layer per iteration on the selected axes. Class 1 fixtures check exact masks, and two archive tests compare reconstruction data with stored exemplars. The filter now rejects non-positive iteration counts and applies each direction flag. ## Algorithm Relationship *Classification:* Port -*Evidence:* `src/Plugins/SimplnxCore/src/SimplnxCore/SimplnxCoreLegacyUUIDMapping.hpp` maps the legacy UUID to this filter; the DREAM.3D v6.5.171 source uses the same six-neighbour rules, while PR #1568 changes storage access to rolling slice buffers. +*Evidence:* `src/Plugins/SimplnxCore/src/SimplnxCore/SimplnxCoreLegacyUUIDMapping.hpp` maps the legacy UUID to this filter; the DREAM.3D v6.5.171 source uses the same six-neighbour rules, and SIMPLNX uses rolling slice buffers for storage access. ## Oracle @@ -41,16 +41,16 @@ *Applied:* Hand-derived true-index sets and four-cell literal arrays specify synchronous dilation, erosion, boundary clipping and axis restrictions. -*Encoded:* `src/Plugins/SimplnxCore/test/ErodeDilateMaskTest.cpp` — seven `Class 1` cases use three fixture layouts; `SimplnxCore::ErodeDilateMaskFilter: Literal One-Dimensional Morphology` adds two axis layouts and ten sections with exact mask and unchanged-sibling checks. All pass in CI at `01809c236`. +*Encoded:* `src/Plugins/SimplnxCore/test/ErodeDilateMaskTest.cpp` — seven `Class 1` cases use three fixture layouts; `SimplnxCore::ErodeDilateMaskFilter: Literal One-Dimensional Morphology` adds two axis layouts and ten sections with exact mask and unchanged-sibling checks. All pass in CI. -*Second-engineer review:* Nathan Young, 2026-08-25, as the approving reviewer of PR #1717. +*Second-engineer review:* Nathan Young, 2026-08-25. ## Bugs found and fixed -All defects in this table are fixed. The fixes will be in the DREAM3D-NX release after v7.4.2. PR #1568 fixed `ErodeDilateMaskFilter-D2`, and PR #1717 fixed `ErodeDilateMaskFilter-D1`. +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 | -|-----------|--------|----------------------------|---------------------------| +|-----------|--------|----------------------------|------------| | `ErodeDilateMaskFilter-D1` | A non-positive iteration count returned success without changing the mask. | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was not affected. | Preflight rejects values below 1 with error `-14701`. | | `ErodeDilateMaskFilter-D2` | Disabled X, Y or Z directions still participated in morphology. | DREAM3D-NX v7.4.1 through v7.4.2. DREAM.3D v6.5.171 was not affected. | The algorithm applies each direction flag to its two face neighbours. | @@ -121,4 +121,4 @@ See `vv/deviations/ErodeDilateMaskFilter.md` for the root cause, affected users, | Deviation | Observed difference | |-----------|---------------------| -| `ErodeDilateMaskFilter-D1` | Historical difference, retired 2026-08-19: SIMPLNX accepted non-positive iteration counts, while DREAM.3D v6.5.171 rejected them. Both now reject these values; the error codes differ (`-14701` and `-5555`). | +| `ErodeDilateMaskFilter-D1` | Retired 2026-08-19: DREAM3D-NX v7.0.0 through v7.4.2 accepted non-positive iteration counts, while DREAM.3D v6.5.171 rejected them. Both now reject these values; the error codes differ (`-14701` and `-5555`). | diff --git a/src/Plugins/SimplnxCore/vv/deviations/ErodeDilateMaskFilter.md b/src/Plugins/SimplnxCore/vv/deviations/ErodeDilateMaskFilter.md index 41d206f35e..87a7b7f050 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/ErodeDilateMaskFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/ErodeDilateMaskFilter.md @@ -1,6 +1,6 @@ # Deviations from DREAM.3D v6.5.171: ErodeDilateMaskFilter -This file has two parts. Part 1 records the historical validation difference from DREAM.3D v6.5.171 (D1). Part 2 records the fixed SIMPLNX direction-flag defect (D2). Both fixes are in develop. +This file has two parts. Part 1 records the validation difference from DREAM.3D v6.5.171 (D1). Part 2 records the fixed SIMPLNX direction-flag defect (D2). Both fixes will be in the DREAM3D-NX release after v7.4.2. Entries use stable IDs (`ErodeDilateMaskFilter-D`) from one sequence. The Filter UUID is the permanent cross-reference anchor. D1 keeps its original ID and retirement date; that date records the V&V decision, not a released fix. @@ -15,18 +15,16 @@ Entries use stable IDs (`ErodeDilateMaskFilter-D`) from one sequence. The Fil | **Deviation ID** | `ErodeDilateMaskFilter-D1` | | **Filter UUID** | `cab66cd1-f64c-42b4-8f94-18f0835a967f` | | **Status** | retired 2026-08-19 | -| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was not affected. | -| **Fixed in** | PR #1717 (`ba246bc7c`), the DREAM3D-NX release after v7.4.2. | **Symptom:** SIMPLNX accepted a zero or negative iteration count and returned the input mask unchanged; DREAM.3D v6.5.171 rejected the same setting. -**Root cause:** Bug. The port omitted the positive-iteration check from `ErodeDilateMask::dataCheck()`. The algorithm loop did not run when `NumIterations < 1`. Current `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ErodeDilateMaskFilter.cpp:94-107` rejects these values with `-14701` and includes the value and mask path in the message. DREAM.3D v6.5.171 source, tag `v6.5.171`, `Source/Plugins/Processing/ProcessingFilters/ErodeDilateMask.cpp:137-142`, uses `-5555` for the same condition. +**Root cause:** Bug. The port omitted the positive-iteration check from `ErodeDilateMask::dataCheck()`. The algorithm loop did not run when `NumIterations < 1`. Current `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ErodeDilateMaskFilter.cpp:94-107` rejects these values with `-14701` and includes the value and mask path in the message. DREAM.3D v6.5.171 source, `Source/Plugins/Processing/ProcessingFilters/ErodeDilateMask.cpp:137-142`, uses `-5555` for the same condition. -**Affected users:** Users whose scripts or saved pipelines supply an iteration count below 1. Earlier SIMPLNX execution silently skipped morphology. +**Affected users:** Users of DREAM3D-NX v7.0.0 through v7.4.2 whose scripts or saved pipelines supply an iteration count below 1. These releases silently skipped morphology. DREAM.3D v6.5.171 was not affected. -**Recommendation:** Trust current SIMPLNX. Supply at least one iteration; the filter now reports invalid counts before execution. The error code differs from DREAM.3D v6.5.171, but both reject the input. +**Recommendation:** Trust current SIMPLNX. Supply at least one iteration; the filter now reports invalid counts before execution. The error code differs from DREAM.3D v6.5.171, but both reject the input. The fix will be in the DREAM3D-NX release after v7.4.2. -**Evidence:** `SimplnxCore::ErodeDilateMaskFilter: Invalid Number of Iterations` checks invalid preflight and error `-14701` for 0 and -2. The requester chose the legacy validation behavior on 2026-08-19; no parameter key changed, and `parametersVersion()` remains 1. The zero-iteration comparison is recorded in `vv_work:ErodeDilateMaskFilter/ReadMe.md`, with inputs in `vv_work:ErodeDilateMaskFilter/extra/legacy_numiter0.json` and `vv_work:ErodeDilateMaskFilter/extra/nx_numiter0.d3dpipeline`. The test passes in CI at `01809c236`. All release branches from `upstream/release/v7.0.0` through `upstream/release/v7.4.2` omit the guard in `ErodeDilateMaskFilter.cpp`. +**Evidence:** PR #1717 (`ba246bc7c`) adds the positive-iteration check. `SimplnxCore::ErodeDilateMaskFilter: Invalid Number of Iterations` checks invalid preflight and error `-14701` for 0 and -2. The requester chose the legacy validation behavior on 2026-08-19; no parameter key changed, and `parametersVersion()` remains 1. The zero-iteration comparison is recorded in `vv_work:ErodeDilateMaskFilter/ReadMe.md`, with inputs in `vv_work:ErodeDilateMaskFilter/extra/legacy_numiter0.json` and `vv_work:ErodeDilateMaskFilter/extra/nx_numiter0.d3dpipeline`. The test passes in CI at `01809c236`. All release branches from `upstream/release/v7.0.0` through `upstream/release/v7.4.2` omit the guard in `ErodeDilateMaskFilter.cpp`. --- @@ -40,18 +38,14 @@ Entries use stable IDs (`ErodeDilateMaskFilter-D`) from one sequence. The Fil | **Filter UUID** | `cab66cd1-f64c-42b4-8f94-18f0835a967f` | | **Status** | active | | **Affected released versions** | DREAM3D-NX v7.4.1 through v7.4.2. DREAM.3D v6.5.171 was not affected. | -| **Fixed in** | PR #1568 (`4ec21e93f`), the DREAM3D-NX release after v7.4.2; PR #1717 (`ba246bc7c`) moves the gating into the shared helper. | +| **Fixed in** | The DREAM3D-NX release after v7.4.2 | **Symptom:** Disabling X, Y or Z still allowed growth or erosion along that axis. -**Root cause:** Bug. PR #1523 (`9bfcb01e4`, 2026-02-05) replaced the explicit face checks with neighbour utilities but omitted direction gating. Its parent reads all three flags; the changed algorithm reads none. This was a refactor regression, not an omission in the original port. PR #1568 restores explicit per-axis gates. Current `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ErodeDilateMask.cpp:156` calls `adjustValidNeighbors`; `src/simplnx/Utilities/NeighborUtilities.hpp:329-337` applies each flag to the two matching faces. The DREAM.3D v6.5.171 source gates all six neighbours on the corresponding axis flags. +**Root cause:** Bug. A change from explicit face checks to neighbour utilities omitted the direction checks. The filter now applies each direction flag through `adjustValidNeighbors` in `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ErodeDilateMask.cpp`. The helper in `src/simplnx/Utilities/NeighborUtilities.hpp` applies each flag to the two matching faces. DREAM.3D v6.5.171 also checks the corresponding axis flag for each of the six neighbours. -**Affected users:** Users who disable an axis with valid face neighbours in DREAM3D-NX releases that contain the regression. All-directions-on examples cannot detect this defect. +**Affected users:** Users who disable an axis with valid face neighbours in DREAM3D-NX v7.4.1 through v7.4.2. All-directions-on examples cannot detect this defect. -**Recommendation:** Trust current SIMPLNX. The algorithm now honors each selected axis. The release after v7.4.2 contains the correction. +**Recommendation:** Trust current SIMPLNX. The algorithm now honors each selected axis. The DREAM3D-NX release after v7.4.2 will contain the correction. -**Evidence:** `Class 1 dilate honours direction flags` checks X-off, `Class 1 dilate with Y direction off` checks Y-off, and `Class 1 erode honours direction flags` checks Z-off. The current one-dimensional test also checks disabled-axis dilation and erosion on X and Z. These tests are in `src/Plugins/SimplnxCore/test/ErodeDilateMaskTest.cpp`. - -**Release-source evidence:** `upstream/release/v7.0.0` through `upstream/release/v7.4.0` gate all six faces in `Filters/Algorithms/ErodeDilateMask.cpp`. `upstream/release/v7.4.1` and `upstream/release/v7.4.2` do not read the direction flags in that algorithm. DREAM.3D tag `v6.5.171`, `Source/Plugins/Processing/ProcessingFilters/ErodeDilateMask.cpp:227-250`, gates all six faces. Both fixes are in develop and will be in the DREAM3D-NX release after v7.4.2. - -**Comparison evidence:** Run against DREAM.3D v6.5.171 on 2026-08-18: 28 of 28 mask cases match after the direction fix. See `vv_work:ErodeDilateMaskFilter/ReadMe.md` and `vv_work:ErodeDilateMaskFilter/results_compare.txt`. The direction-flag regression tests pass in CI at `01809c236`. +**Evidence:** PR #1523 (`9bfcb01e4`) removed direction checks; PR #1568 (`4ec21e93f`) restored them, and PR #1717 (`ba246bc7c`) moved them into the shared helper. In `src/Plugins/SimplnxCore/test/ErodeDilateMaskTest.cpp`, the Class 1 direction tests check X-off, Y-off and Z-off; the one-dimensional test checks disabled-axis dilation and erosion on X and Z. These tests pass in CI at `01809c236`. The algorithm in `upstream/release/v7.0.0` through `upstream/release/v7.4.0` checks all six faces; `upstream/release/v7.4.1` and `upstream/release/v7.4.2` do not read the direction flags. DREAM.3D v6.5.171 checks all six faces in `Source/Plugins/Processing/ProcessingFilters/ErodeDilateMask.cpp:227-250`. The comparison recorded on 2026-08-18 has 28 of 28 matches; see `vv_work:ErodeDilateMaskFilter/ReadMe.md` and `vv_work:ErodeDilateMaskFilter/results_compare.txt`. That run predates the change to rolling slice buffers and does not verify the current algorithm. From fc4e928128d52de0ac29ecc3e9f308818bb60c55 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 20:01:28 -0400 Subject: [PATCH 54/91] DOC: Rewrite the FillBadData V&V record to follow the report template Signed-off-by: Michael Jackson --- .../SimplnxCore/vv/FillBadDataFilter.md | 204 ++++++++++-------- .../vv/deviations/FillBadDataFilter.md | 163 ++++++++++++-- .../vv/provenance/FillBadDataFilter.md | 76 ++++--- 3 files changed, 295 insertions(+), 148 deletions(-) diff --git a/src/Plugins/SimplnxCore/vv/FillBadDataFilter.md b/src/Plugins/SimplnxCore/vv/FillBadDataFilter.md index c82cf2da2e..00d2d2423d 100644 --- a/src/Plugins/SimplnxCore/vv/FillBadDataFilter.md +++ b/src/Plugins/SimplnxCore/vv/FillBadDataFilter.md @@ -1,123 +1,137 @@ # V&V Report: FillBadDataFilter -| | | -|-----------|------------------| -| Plugin | SimplnxCore | -| SIMPLNX UUID | `a59eb864-9e6b-40bb-9292-e5281b0b4f3e` | -| SIMPLNX Human Name | Fill Bad Data | -| DREAM3D 6.5.171 equivalent | `FillBadData` — SIMPL UUID `30ae0a1e-3d94-5dab-b279-c5727ab5d7ff` | -| Verified commit | ** | -| Status | COMPLETE — 2026-07-16 | -| Sign-off | Michael Jackson — 2026-07-16 | +| | | +|--------|--------------| +| Plugin | SimplnxCore | +| SIMPLNX UUID | `a59eb864-9e6b-40bb-9292-e5281b0b4f3e` | +| SIMPLNX Human Name | Fill Bad Data | +| DREAM.3D v6.5.171 equivalent | `FillBadData` (Processing plugin, SIMPL UUID `30ae0a1e-3d94-5dab-b279-c5727ab5d7ff`) | +| Verified commit | ** | +| Status | COMPLETE | +| Sign-off | Nathan Young, 2026-07-03; second engineer: Michael Jackson, 2026-07-10 | ## At a glance -| Aspect | Current state | -|------------------------|--------------------------| -| Algorithm Relationship | **Rewrite** — 4-phase chunk-sequential CCL+Union-Find (OOC support); legacy used a simpler in-memory approach. Functional behavior preserved. | -| Oracle (confirmed) | **Class 2** for `FillBadData_SmallIN100` (`6_5_exemplar.dream3d` from legacy 6.5 pipeline). **Class 1** for Tests 01–13 (hand-authored expected values serialized by a format-conversion script that never runs `FillBadDataFilter`). Circular-oracle concern resolved. | -| Code paths enumerated | 20 of 21 exercised; cancellation remains outside the scope. | -| Tests today | 18 registered cases plus two hidden ones: the real-HDF5 slice-boundary oracle with the new-phase option off/on, and the test-data generator, which is a fixture-generation utility, not an oracle. | -| Exemplar archive | `6_5_fill_bad_data.tar.gz` — `6_5_input/exemplar.dream3d` (Class 2) + `test_NN_input/expected.dream3d` pairs for Tests 01–07, 11, 13 (Class 1). *(Tests 08–10 and 12 have no fixtures — the numbering is non-contiguous.)* | -| Legacy comparison | SmallIN100 in-test (Class 2) — SIMPLNX matches the 6.5.x exemplar element-wise. A separate Test 08 three-way binary A/B was cited by an earlier revision but its working files are unrecoverable, so that claim is **withdrawn** (see deviations). Per-code-path correctness is pinned by the Class 1 analytical fixtures. | -| Bug flags | `FillBadDataFilter-B1` (preflight dead-return for `minAllowedDefectSize < 1`) resolved. Resolved: an all-bad-data / enclosed-bad-pocket input previously looped forever in Phase 4 (no fillable neighbor → `count` never reached 0); a no-progress guard now stops with a warning. | -| 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 | Rewrite of `FillBadData` in DREAM.3D v6.5.171. SIMPLNX selects BFS or slice-based connected-component labeling (CCL). | +| Oracle (confirmed) | Class 1 expected arrays cover nine synthetic archive fixtures in `test/FillBadDataTest.cpp`. All pass in CI. | +| Code paths enumerated | 24 of 40 exercised. Cancellation, failure injection, large-index limits and the no-progress guard lack direct coverage. | +| Tests today | 20 test cases: 18 ordinary cases, one hidden HDF5 fixture and one hidden data generator. Ordinary cases cover analytical outputs, exemplar regressions, validation, ignored arrays and SIMPL conversion. | +| Exemplar archive | `6_5_fill_bad_data.tar.gz` supplies nine analytical input/output pairs and the Small IN100 regression pair. | +| Legacy comparison | Not run on the current algorithm. The comparison record is dated 2026-07-03; algorithm changes followed that run. | +| Bug flags | None | +| V&V phase | Analytical fixtures and regression tests are encoded. All pass in CI. | ## Summary -`FillBadDataFilter` fills voxels with `FeatureId == 0` ("bad data") by assigning them to the most-common positive-feature neighbor. Connected regions below `minAllowedDefectSize` are filled; larger regions are preserved (optionally relabeled as a new phase). SIMPLNX uses a four-phase CCL+Union-Find architecture to support OOC datasets — a structural departure from legacy DREAM3D 6.5.171. Output equivalence is supported by the `FillBadData_SmallIN100` unit test (passes against `6_5_exemplar.dream3d`). A previously cited manual A/B run on a custom dataset across DREAM3D 6.5.171, a locally patched legacy build, and DREAM3D-NX is withdrawn because its working files are unrecoverable (see deviations). One bug surfaced during V&V (B1 — dead error-return path for `minAllowedDefectSize < 1`) has been fixed and is now covered by a dedicated test. +`FillBadDataFilter` fills connected zero-FeatureId regions smaller than the selected threshold by copying positive face-neighbor tuples. It preserves larger regions and can assign them a new phase. Class 1 expected arrays check the output; all ordinary tests pass in CI. ## Algorithm Relationship -*Classification:* **Rewrite** ~~| Port | Minor changes | New filter~~ +*Classification:* Rewrite -The SIMPLNX algorithm (`Algorithms/FillBadData.cpp`, ~765 lines) introduces a 4-phase architecture to support OOC datasets stored in chunked Zarr format: - -1. **Phase 1 — Chunk-sequential CCL:** Assigns provisional negative labels to connected components of bad data using a scanline algorithm. `ChunkAwareUnionFind` tracks cross-chunk label equivalences. -2. **Phase 2 — Global resolution:** Flattens the Union-Find tree (path compression + size accumulation to roots). No equivalent in legacy. -3. **Phase 3 — Region classification:** Compares each component's voxel count to `minAllowedDefectSize`; small → mark -1 for fill, large → keep as 0 or assign new phase. -4. **Phase 4 — Iterative fill:** Assigns each -1 voxel to its most-common positive-feature face neighbor, iterating until none remain. Propagates all cell-data arrays via `FillBadDataUpdateTuplesFunctor`. - -SIMPL UUID mapping is preserved via `SimplnxCoreLegacyUUIDMapping.hpp` and SIMPL conversion fixtures (`test/simpl_conversion/6_5/` and `6_4/FillBadDataFilter.json`). - -*Behavioral equivalence evidence:* `FillBadData_SmallIN100` passes against `6_5_exemplar.dream3d` (Small IN100, threshold=1000). (A separate Test 08 three-way A/B was previously cited but its working files are unrecoverable; that claim is withdrawn — see deviations.) +*Evidence:* `src/Plugins/SimplnxCore/src/SimplnxCore/SimplnxCoreLegacyUUIDMapping.hpp` maps the legacy UUID to this filter; BFS and CCL replace the single legacy flood-fill implementation while retaining the size rule and neighbor voting. ## Oracle -*Class:* **Class 2 (Reference implementation)** for `FillBadData_SmallIN100`. **Class 1 (Analytically derived)** for Tests 01–13. - -**Class 2:** `6_5_fill_bad_data/6_5_exemplar.dream3d` was generated by running a DREAM3D 6.5.x `FillBadData` pipeline against the Small IN100 dataset (`MinAllowedDefectSize=1000`, `StoreAsNewPhase=false`). The legacy filter is the independent reference. - -**Class 1:** `test_NN_expected_featureids.txt` files were hand-authored by tracing the algorithm specification phase-by-phase. `generate_dream3d_files.py` serializes these arrays to HDF5 without running `FillBadDataFilter`. Evidence of independent derivation: Test 11 explicitly traces neighbor scan order and identifies Feature 1 as the tie-break winner by count — reasoning that cannot originate from a simulation. +*Class:* 1 (Analytical). -**Encoded tests:** +*Applied:* Hand-derived FeatureIds and Phases define component retention, filling and the first-to-maximum neighbor vote rule; the archive script only serializes these arrays. -- Class 2: `"SimplnxCore::FillBadData_SmallIN100"` — `UnitTest::CompareExemplarToGeneratedData` against `6_5_exemplar.dream3d`. Passes. -- Class 1: `"SimplnxCore::FillBadData::Test01"` through `Test13` — `UnitTest::CompareExemplarToGeneratedData` against `test_NN_expected.dream3d`. Pass. -- SIMPL compat: `"SimplnxCore::FillBadDataFilter: SIMPL Backwards Compatibility"` — UUID + arg-key + value assertions only. Not an oracle test. +*Encoded:* `src/Plugins/SimplnxCore/test/FillBadDataTest.cpp` — `Test01_SingleSmallDefect` through `Test07_DefectsAtBoundaries`, `Test11_NeighborTieBreaking` and `Test13_StoreAsNewPhase`: nine fixtures. 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, is named in the oracle review record. ## 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 the fixes for `FillBadDataFilter-D1` through `FillBadDataFilter-D5`. The fixes for `FillBadDataFilter-D6` and `FillBadDataFilter-D7` will be in the DREAM3D-NX release after v7.4.2. + +| Deviation | Defect | Affected released versions | Resolution | +|-----------|--------|----------------------------|------------| +| `FillBadDataFilter-D1` | Preflight discarded the error for a minimum defect size below 1. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | Return error -16500 before execution. | +| `FillBadDataFilter-D2` | Unfillable negative cells could keep the fill loop running forever. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | Stop with a warning when no cell has a fill source. | +| `FillBadDataFilter-D3` | Negative input FeatureIds could corrupt the vote-array allocation size through unsigned conversion. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | Use only positive FeatureIds to determine the maximum ID. | +| `FillBadDataFilter-D4` | A component size above INT32_MAX could become negative and be classified as small. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | Compare component sizes without narrowing to Int32. | +| `FillBadDataFilter-D5` | A neighbor index above INT32_MAX could overflow and select an invalid tuple. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | Keep neighbor indices in 64-bit storage. | +| `FillBadDataFilter-D6` | FeatureIds from another geometry could be processed with the selected image dimensions. | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was not affected. | Require FeatureIds in the selected geometry's Cell Attribute Matrix (-16501). | +| `FillBadDataFilter-D7` | An ignored array selected through an alias could still be modified. | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was not affected. | Normalize array paths to the geometry's Cell Attribute Matrix before filtering the ignored arrays. | ## Code path coverage -20 of 21 paths exercised. Cancellation is not directly tested. +24 of 40 paths exercised. Each uncovered row states the missing fixture. + +Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/FillBadData.cpp` (49 lines), `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/FillBadDataBFS.cpp` (378 lines), `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/FillBadDataCCL.cpp` (917 lines), and `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/FillBadDataFilter.cpp` (194 lines). -Source: `Algorithms/FillBadData.cpp` (49 lines), `FillBadDataBFS.cpp` (378 lines), and `FillBadDataCCL.cpp` (903 lines). +Test names below omit the common plugin and filter prefixes. BFS visits connected cells in memory; CCL labels slices and resolves component sizes before filling. | # | Phase | Path | Test case | |---|-------|------|-----------| -| 1 | Phase 1 — CCL | Non-bad voxel (`featureId ≠ 0`) → skip | All tests | -| 2 | Phase 1 — CCL | Bad voxel, no bad backward neighbors → new negative label | `Test01_SingleSmallDefect` | -| 3 | Phase 1 — CCL | Bad voxel, one bad backward neighbor → inherit label | `Test01`, `Test02`, `Test03` | -| 4 | Phase 1 — CCL | Bad voxel, bad backward neighbors from different labels → `unionFind.unite()` | `Test04`, `Test07` | -| 5 | Phase 2 — Resolution | `flatten()` — always executes | All tests | -| 6 | Phase 3 — Relabeling | Region < threshold → mark -1 | `Test01`, `Test06`, `FillBadData_SmallIN100` | -| 7 | Phase 3 — Relabeling | Region ≥ threshold, `storeAsNewPhase=false` → keep as 0 | `Test02`, `Test03` | -| 8 | Phase 3 — Relabeling | Region > threshold, `storeAsNewPhase=true`, `cellPhasesPtr ≠ nullptr` → `phase[i] = maxPhase+1` | `Test13_StoreAsNewPhase` | -| 9 | Phase 4 — Fill | FeatureId ≥ 0 → skip | All tests | -| 10 | Phase 4 — Fill | FeatureId = -1, ≥1 positive neighbor → vote and assign | `Test01`, `Test06`, `FillBadData_SmallIN100` | -| 11 | Phase 4 — Fill | FeatureId = -1, no positive neighbor this iteration → deferred to next iteration | `Test04` (interior voxels) | -| 12 | Phase 4 — Fill | Neighbor tie-break → first-encountered wins by scan order | `Test11_NeighborTieBreaking` | -| 13 | `operator()` | `storeAsNewPhase=false` → `cellPhasesPtr = nullptr`, phases loop skipped | Tests 01–07, 11, SmallIN100 | -| 14 | `operator()` | `m_ShouldCancel` in Phase 4 while-loop | *Not covered.* No test exercises mid-fill cancellation. | -| 15 | Preflight | `minAllowedDefectSize < 1` → `MakePreflightErrorResult(-16500, …)` | `"SimplnxCore::FillBadDataFilter:: Invalid Preflight Min Defect Size"` — inline DataStructure, `minAllowedDefectSize=0`; asserts error code `-16500`. | -| 16 | Phase 4 | no fillable neighbor for any remaining bad voxel → no-progress break (warning) | `AllBadData_TerminatesWithoutHang` — all-bad-data slab; asserts the filter returns instead of looping forever. | -| 17 | CCL storage | A threshold-sized defect spans rolling slices, and a smaller defect copies a multi-component tuple | `real HDF5 cross-slice defect oracle` — exact full-volume FeatureIds, Phases, and payload | -| 18 | Preflight | Cell Feature Ids is not in the selected geometry's Cell Attribute Matrix → error `-16501` | `SimplnxCore::FillBadDataFilter: Strict Feature Ids` — asserts invalid preflight and exact code `-16501`. | -| 19 | Preflight | Store Defects as New Phase is on and Cell Phases is not in the selected geometry's Cell Attribute Matrix → error `-16502` | `SimplnxCore::FillBadDataFilter: Strict Feature Ids` — asserts invalid preflight and exact code `-16502`; also checks valid Cell Phases and unused external Cell Phases. | -| 20 | Preflight | An Attribute Arrays to Ignore entry is not in the selected geometry's Cell Attribute Matrix → error `-16503` | `SimplnxCore::FillBadDataFilter: Strict Feature Ids` — asserts invalid preflight and exact code `-16503`. | -| 21 | Phase 4 — Fill | An ignored array selected through an alias of the Cell Attribute Matrix is excluded from modification | `SimplnxCore::FillBadDataFilter: Ignored Array Cell AM Alias` — checks unchanged ignored values for each in-memory algorithm scenario. | +| 1 | Dispatch | Select BFS for resident arrays | `Test01_SingleSmallDefect` through `Test13_StoreAsNewPhase` — algorithm scenarios | +| 2 | Dispatch | Select CCL for resident arrays | `Test01_SingleSmallDefect` through `Test13_StoreAsNewPhase` — algorithm scenarios | +| 3 | Dispatch | Select CCL for external arrays | *Not directly tested. The hidden `real HDF5 cross-slice defect oracle` requires external-store execution.* | +| 4 | Components | Skip nonzero FeatureIds | `Test01_SingleSmallDefect` | +| 5 | Components | Start a component with no labeled backward neighbor | `Test06_SingleVoxelDefects` | +| 6 | Components | Inherit a backward neighbor label or visit a connected BFS neighbor | `Test02_SingleLargeDefect` | +| 7 | Components | Merge distinct CCL labels | *Not directly tested. No fixture isolates a merge of distinct labels.* | +| 8 | Classification | Region is smaller than the threshold; mark it for filling | `Test01_SingleSmallDefect`; `Test05_MixedSmallAndLarge` | +| 9 | Classification | Region is at or above the threshold; keep FeatureIds zero | `Test02_SingleLargeDefect`; `Test03_ThresholdBoundary` | +| 10 | Classification | Store Defects as New Phase is enabled; assign maximum phase plus one | `Test13_StoreAsNewPhase`; `200x200x200 Correctness` / `Variant: NewPhase` | +| 11 | Fill | Skip nonnegative FeatureIds and out-of-image neighbors | `Test07_DefectsAtBoundaries` | +| 12 | Fill | Copy the selected positive neighbor into a small defect | `Test01_SingleSmallDefect`; `FillBadData_SmallIN100` | +| 13 | Fill | Defer interior cells until a later iteration | `Test04_MultipleSmallDefects` | +| 14 | Fill | Break equal vote totals by the first feature to reach that total | `Test11_NeighborTieBreaking` | +| 15 | Setup | Store Defects as New Phase is disabled; omit the maximum-phase scan | `Test01_SingleSmallDefect`; `200x200x200 Correctness` / `Variant: NoNewPhase` | +| 16 | Cancellation | Stop component labeling, classification, fill or tuple transfer | *Not directly tested. No fixture injects cancellation.* | +| 17 | Preflight | Reject a minimum defect size below 1 with -16500 | `Invalid Preflight Min Defect Size` | +| 18 | Fill | Stop with a warning when remaining negative cells have no positive neighbor | *Not directly tested. `AllBadData_TerminatesWithoutHang` retains its nine-cell region at threshold 1, so it does not reach this guard.* | +| 19 | Setup | Exclude negative input FeatureIds from maximum-ID allocation sizing | *Not directly tested. No fixture supplies negative input FeatureIds.* | +| 20 | Classification | Compare a region size above INT32_MAX without narrowing | *Not directly tested. The fixtures contain fewer than INT32_MAX cells.* | +| 21 | Fill | Store a neighbor index above INT32_MAX without narrowing | *Not directly tested. The fixtures contain fewer than INT32_MAX cells.* | +| 22 | Preflight | Reject FeatureIds outside the selected Cell Attribute Matrix with -16501 | `Strict Feature Ids` | +| 23 | Preflight | Reject modified Phases outside the selected Cell Attribute Matrix with -16502 | `Strict Feature Ids` | +| 24 | Preflight | Reject an ignored array outside the selected Cell Attribute Matrix with -16503 | `Strict Feature Ids` | +| 25 | Paths | Normalize aliases before modification reporting and ignored-array selection | `Ignored Array Cell AM Alias` | +| 26 | Preflight | Warn that feature data must be recomputed | `Preflight Feature Data Modification Warning` — -14600 and output-value name | +| 27 | Fill | Leave ignored arrays unchanged | `200x200x200 Ignored Arrays` | +| 28 | I/O | Propagate array, candidate-pair or equivalence read/write failures | *Not directly tested. No fixture injects storage failures.* | +| 29 | Storage | Fall back to an in-memory temporary store after provider failure | *Not directly tested. No fixture forces provider failure.* | +| 30 | Storage | Reject a null temporary store with -87021 or -87023 | *Not directly tested. No fixture supplies a null provider result.* | +| 31 | Capacity | Reject equivalence or candidate capacity overflow with -87020, -87022 or -87029 | *Not directly tested. These limits require synthetic capacity injection.* | +| 32 | Capacity | Reject an empty image with -87027 | *Not directly tested. All fixtures contain cells.* | +| 33 | Capacity | Reject label-range exhaustion with -87028 or -87030 | *Not directly tested. No fixture reaches the Int32 label limit.* | +| 34 | Transfer | Reject zero or overflowing slice buffers with -87024 | *Not directly tested. All fixtures have valid slice and component sizes.* | +| 35 | Transfer | Reject a short candidate read with -87025 | *Not directly tested. No fixture supplies a short read.* | +| 36 | Transfer | Reject a candidate from a nonadjacent slice with -87026 | *Not directly tested. Normal candidate generation uses face neighbors.* | +| 37 | Transfer | Process full candidate batches and a final partial batch | `200x200x200 Correctness` | +| 38 | Conversion | Convert SIMPL parameters, with the ignored-array key present or absent | `SIMPL Backwards Compatibility` / `SIMPL 6.5 (UUID)` and `SIMPL 6.4 (Filter_Name)` | +| 39 | Transfer | Copy every component of a cell tuple | `FillBadData_SmallIN100` — compares the three-component EulerAngles array | +| 40 | Preflight | Accept an unused Phases path outside the Cell Attribute Matrix | `Strict Feature Ids` / `Unused Cell Phases in another Attribute Matrix` | ## Test inventory | Test case | Status | Notes | |-----------|--------|-------| -| `SimplnxCore::FillBadData_SmallIN100` | kept | Class 2 oracle. Both algorithms on resident stores. threshold=1000, storeAsNewPhase=false. | -| `SimplnxCore::FillBadData::Test01_SingleSmallDefect` | kept | Resident-store algorithm-equivalence test. threshold=20. Single small region — primary fill path. Class 1. | -| `SimplnxCore::FillBadData::Test02_SingleLargeDefect` | kept | Resident-store algorithm-equivalence test. threshold=20. Single large region kept as 0. Class 1. | -| `SimplnxCore::FillBadData::Test03_ThresholdBoundary` | kept | Resident-store algorithm-equivalence test. threshold=25. Exact-threshold boundary (≥ kept, < filled). Class 1. | -| `SimplnxCore::FillBadData::Test04_MultipleSmallDefects` | kept | Resident-store algorithm-equivalence test. threshold=50. Multiple disconnected small regions — multi-iteration fill. Class 1. | -| `SimplnxCore::FillBadData::Test05_MixedSmallAndLarge` | kept | Resident-store algorithm-equivalence test. threshold=50. Mixed small (filled) and large (kept). Class 1. | -| `SimplnxCore::FillBadData::Test06_SingleVoxelDefects` | kept | Resident-store algorithm-equivalence test. threshold=10. Single-voxel bad-data islands. Class 1. | -| `SimplnxCore::FillBadData::Test07_DefectsAtBoundaries` | kept | Resident-store algorithm-equivalence test. threshold=20. Regions at image boundary — exercises Phase 1 CCL boundary handling. Class 1. | -| `SimplnxCore::FillBadData::Test11_NeighborTieBreaking` | kept | Resident-store algorithm-equivalence test. threshold=10. Tie-break via scan order. Class 1. | -| `SimplnxCore::FillBadData::Test13_StoreAsNewPhase` | kept | Resident-store algorithm-equivalence test. threshold=20, storeAsNewPhase=true. Only test for `cellPhasesPtr ≠ nullptr` path (Path 8). Class 1. | -| `SimplnxCore::FillBadDataFilter: Preflight Feature Data Modification Warning` | kept | Inline 2×2×2 DataStructure, threshold=1, storeAsNewPhase=false, no ignored arrays. Asserts a valid preflight, warning `-14600`, a `Feature Data Modification Warning` preflight value, and no `Stale Arrays` preflight value. Not an oracle test. | -| `SimplnxCore::FillBadDataFilter:: Invalid Preflight Min Defect Size` | kept | Inline DataStructure (no file load). `minAllowedDefectSize=0` → asserts invalid **and** error code `-16500`. Covers Path 15. | -| `SimplnxCore::FillBadData::AllBadData_TerminatesWithoutHang` | kept | Inline 3×3×1 all-bad-data slab (no good neighbor). Asserts the filter returns rather than looping forever. Covers Path 16 (no-progress guard). | -| `SimplnxCore::FillBadDataFilter: SIMPL Backwards Compatibility` | kept | SIMPL 6.4 + 6.5 via `DYNAMIC_SECTION`. UUID + arg-key + value assertions only. Not an oracle test. | -| `real HDF5 cross-slice defect oracle` | new-for-V&V | Independent HDF5 boundary outputs; 1053 assertions across the configured options. | -| `200x200x200 Correctness` | kept | Existing large generated-data correctness test; algorithm scopes keep its stores resident. | -| `200x200x200 Ignored Arrays` | kept | Existing ignored-array preservation regression on resident stores. | -| `SimplnxCore::FillBadDataFilter: Strict Feature Ids` | new-for-V&V | Six SECTIONs: correct Cell Attribute Matrix; Feature Ids in another Attribute Matrix (`-16501`); ignored array in another Attribute Matrix (`-16503`); modified Cell Phases in another Attribute Matrix (`-16502`); unused Cell Phases in another Attribute Matrix; modified Cell Phases in the Cell Attribute Matrix. Checks preflight validity, exact error codes, and diagnostic paths. Covers Paths 18–20. | -| `SimplnxCore::FillBadDataFilter: Ignored Array Cell AM Alias` | new-for-V&V | Cell Attribute Matrix reachable by two paths. Selects the ignored array through the alias path and runs each in-memory algorithm scenario. Checks that Feature Ids change while the ignored array remains unchanged. Covers Path 21. | -| `Generate Test Data` | kept | Hidden (`[.GenerateTestData]`). Creates a 200×200×200 fixture with a full-plane defect and writes `generated_test_data/fill_bad_data/input.dream3d` under the binary test-output directory. Checks that buffer writes and file output succeed. Does not execute FillBadData or compare algorithm output. | - -OOC recertification (2026-09-21): 15/15 target CTest entries pass in both DREAM3DNX builds. The hidden `real HDF5 cross-slice defect oracle` passes 1053 assertions in the OOC binary. A three-cell defect spans three Z slices and equals the retention threshold. A disconnected one-cell defect must be filled. Exact FeatureIds, Phases, and a two-component payload are checked for every voxel, with StoreAsNewPhase off and on. HDF5 input stores and target-only OOC dispatch counters verify the CCL path. The expected preflight warning -14600 remains present. 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. +| `SimplnxCore::FillBadData_SmallIN100` | kept | Compares Small IN100 cell arrays with the stored legacy exemplar at threshold 1000. This is a regression comparison, not an independent correctness oracle. | +| `SimplnxCore::FillBadData::Test01_SingleSmallDefect` | kept | Class 1. Fills one small component at threshold 20; compares cell arrays with the analytical file. | +| `SimplnxCore::FillBadData::Test02_SingleLargeDefect` | kept | Class 1. Retains a large component at threshold 20. | +| `SimplnxCore::FillBadData::Test03_ThresholdBoundary` | kept | Class 1. Retains a component whose size equals threshold 25. | +| `SimplnxCore::FillBadData::Test04_MultipleSmallDefects` | kept | Class 1. Fills multiple small components at threshold 50. | +| `SimplnxCore::FillBadData::Test05_MixedSmallAndLarge` | kept | Class 1. Fills small components and retains a large component at threshold 50. | +| `SimplnxCore::FillBadData::Test06_SingleVoxelDefects` | kept | Class 1. Fills isolated single-cell defects at threshold 10. | +| `SimplnxCore::FillBadData::Test07_DefectsAtBoundaries` | kept | Class 1. Checks defects at image boundaries at threshold 20. | +| `SimplnxCore::FillBadData::Test11_NeighborTieBreaking` | kept | Class 1. Expects FeatureId 1 at the center of a three-vote tie. | +| `SimplnxCore::FillBadData::Test13_StoreAsNewPhase` | kept | Class 1. Retains 50 defect cells and changes their phase to 2 at threshold 20. | +| `SimplnxCore::FillBadDataFilter: Preflight Feature Data Modification Warning` | kept | Checks warning -14600, the Feature Data Modification Warning output and the absence of a Stale Arrays output. | +| `SimplnxCore::FillBadDataFilter:: Invalid Preflight Min Defect Size` | kept | Checks error -16500 for minimum size zero (FillBadDataFilter-D1). | +| `SimplnxCore::FillBadData::AllBadData_TerminatesWithoutHang` | kept | Checks successful return for nine bad cells at threshold 1. The region is retained; this does not exercise the no-progress guard. | +| `SimplnxCore::FillBadData: real HDF5 cross-slice defect oracle` | new-for-V&V | Hidden. Defines a 5×5×5 fixture with a retained three-cell component across slices and one filled cell; asserts FeatureIds, Phases and a two-component payload with the new-phase option off and on. | +| `SimplnxCore::FillBadDataFilter: SIMPL Backwards Compatibility` | kept | Checks converted UUID and arguments in dynamic sections SIMPL 6.5 (UUID) and SIMPL 6.4 (Filter_Name). | +| `SimplnxCore::FillBadData: 200x200x200 Correctness` | kept | Compares FeatureIds and Phases with `fill_bad_data_exemplars.tar.gz` in dynamic sections Variant: NoNewPhase and Variant: NewPhase. | +| `SimplnxCore::FillBadData: 200x200x200 Ignored Arrays` | kept | Checks that bad FeatureIds change and that every ignored sentinel value remains unchanged. | +| `SimplnxCore::FillBadDataFilter: Strict Feature Ids` | new-for-V&V | Six sections check valid cell selection, errors -16501/-16502/-16503, unused external Phases and valid modified Phases. | +| `SimplnxCore::FillBadDataFilter: Ignored Array Cell AM Alias` | new-for-V&V | Checks that aliased FeatureIds change, ignored values remain unchanged and ignored paths are absent from modification actions. | +| `SimplnxCore::FillBadData: Generate Test Data` | kept | Hidden utility. Writes a synthetic 200×200×200 input file; does not execute FillBadData or assert filter output. | ## Exemplar archive @@ -125,12 +139,14 @@ OOC recertification (2026-09-21): 15/15 target CTest entries pass in both DREAM3 - **SHA512:** `73b6835252135619325d9b02b48286ca95f0235820610bddcc43ec0a3f075337481bace00bf20e9d59744f1d4024cc18be624ec522922a6461b55ae8dbb0e628` - **Provenance:** `src/Plugins/SimplnxCore/vv/provenance/FillBadDataFilter.md` -## Deviations from DREAM3D 6.5.171 - -No deviations observed. The legacy comparison is the SmallIN100 in-test check: - -1. `FillBadData_SmallIN100` passes against `6_5_exemplar.dream3d` (Small IN100, MinAllowedDefectSize=1000) — SIMPLNX matches the 6.5.x exemplar element-wise. +## Deviations from DREAM.3D v6.5.171 -Per-code-path correctness is pinned independently by the Class 1 analytical fixtures (Tests 01–07, 11, 13). (An earlier revision also cited a Test 08 three-way binary A/B; its working files are unrecoverable, so that claim is withdrawn.) +See `vv/deviations/FillBadDataFilter.md` for the root cause, affected users and recommendation. These Part 1 entries describe source-confirmed differences on invalid or extreme inputs. -See `vv/deviations/FillBadDataFilter.md` for the full comparison record. +| Deviation | Observed difference | +|-----------|---------------------| +| `FillBadDataFilter-D1` | SIMPLNX rejects a minimum size below 1; DREAM.3D v6.5.171 accepts it. | +| `FillBadDataFilter-D2` | SIMPLNX stops with a warning when no fill source exists; DREAM.3D v6.5.171 can keep looping. | +| `FillBadDataFilter-D3` | SIMPLNX excludes negative input IDs when sizing vote storage; DREAM.3D v6.5.171 converts them to unsigned values. | +| `FillBadDataFilter-D4` | SIMPLNX compares full-width component sizes; DREAM.3D v6.5.171 narrows them to Int32. | +| `FillBadDataFilter-D5` | SIMPLNX stores 64-bit neighbor indices; DREAM.3D v6.5.171 stores Int32 indices. | diff --git a/src/Plugins/SimplnxCore/vv/deviations/FillBadDataFilter.md b/src/Plugins/SimplnxCore/vv/deviations/FillBadDataFilter.md index 0fadedd0dd..e44b61da51 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/FillBadDataFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/FillBadDataFilter.md @@ -1,40 +1,161 @@ -# Deviations from DREAM3D 6.5.171: FillBadDataFilter +# Deviations from DREAM.3D v6.5.171: FillBadDataFilter -Entries use stable IDs (`FillBadDataFilter-D` for legacy deviations, `FillBadDataFilter-B` for SIMPLNX-side bugs). +Part 1 lists source-confirmed differences from DREAM.3D v6.5.171. Part 2 lists defects in SIMPLNX selection and alias handling. All entries use one stable ID sequence. + +## Part 1 — Differences from DREAM.3D v6.5.171 + +### FillBadDataFilter-D1 + +| Field | Value | +|---|---| +| **Deviation ID** | `FillBadDataFilter-D1` | +| **Filter UUID** | `a59eb864-9e6b-40bb-9292-e5281b0b4f3e` | +| **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 minimum defect size below 1 passed preflight. + +**Root cause:** bug. The error result was created and discarded. Preflight now returns -16500. DREAM.3D v6.5.171 has no check for this invalid minimum size. + +**Affected users:** Users who enter zero or a negative minimum defect size. + +**Recommendation:** Trust SIMPLNX. Use a minimum size of at least 1. + +**Evidence:** `Invalid Preflight Min Defect Size` checks zero and error -16500. PR #1653 (`a9d7e17e3`) fixes the discarded return. The earlier report called this defect `FillBadDataFilter-B1`. Release and legacy source checks are recorded in `../provenance/FillBadDataFilter.md`. + +--- + +### FillBadDataFilter-D2 + +| Field | Value | +|---|---| +| **Deviation ID** | `FillBadDataFilter-D2` | +| **Filter UUID** | `a59eb864-9e6b-40bb-9292-e5281b0b4f3e` | +| **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:** The filter could run forever when negative cells remained without a positive face neighbor. + +**Root cause:** bug. The fill loop counted remaining cells but did not check whether it assigned any source neighbor. Both current implementations stop with a warning when no assignment is possible. Unfilled cells keep their current negative markers. DREAM.3D v6.5.171 also lacks this guard. + +**Affected users:** Users with an all-zero image smaller than the minimum defect size, or negative cells without a positive neighbor. + +**Recommendation:** Trust SIMPLNX. The filter now terminates and reports that some cells could not be filled. + +**Evidence:** PR #1653 (`a9d7e17e3`) adds the guard. `AllBadData_TerminatesWithoutHang` has nine zero cells and threshold 1; its region remains zero, so the test does not exercise the guard. Release and legacy source checks are recorded in `../provenance/FillBadDataFilter.md`. + +--- + +### FillBadDataFilter-D3 + +| Field | Value | +|---|---| +| **Deviation ID** | `FillBadDataFilter-D3` | +| **Filter UUID** | `a59eb864-9e6b-40bb-9292-e5281b0b4f3e` | +| **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:** Negative input FeatureIds could cause an invalid vote-array allocation or access. + +**Root cause:** bug. The maximum-ID scan compared a signed FeatureId with an unsigned maximum. A negative ID became a large unsigned value. Both current implementations count only positive IDs when sizing vote storage. DREAM.3D v6.5.171 uses the same signed/unsigned comparison. + +**Affected users:** Users whose input FeatureIds contain negative values. + +**Recommendation:** Trust SIMPLNX. The maximum-ID scan now excludes negative values from allocation sizing. + +**Evidence:** PR #1653 (`a9d7e17e3`) adds the positive-ID condition. No current test supplies negative input IDs. Release and legacy source checks are recorded in `../provenance/FillBadDataFilter.md`. --- -## Headline: No deviations observed +### FillBadDataFilter-D4 + +| Field | Value | +|---|---| +| **Deviation ID** | `FillBadDataFilter-D4` | +| **Filter UUID** | `a59eb864-9e6b-40bb-9292-e5281b0b4f3e` | +| **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 region larger than INT32_MAX cells could be filled instead of retained. + +**Root cause:** bug. The size comparison narrowed the component count to Int32. Large counts could become negative and pass the small-region test. Current BFS compares `usize` counts; CCL compares `uint64` counts. DREAM.3D v6.5.171 also narrows the component count to Int32. + +**Affected users:** Users with a connected defect region larger than 2,147,483,647 cells. -The legacy comparison is the **SmallIN100 in-unit-test check**: `FillBadData_SmallIN100` compares SIMPLNX output against `6_5_exemplar.dream3d` (generated from a DREAM3D 6.5.x pipeline) with `MinAllowedDefectSize=1000`, `StoreAsNewPhase=false`. It passes — no differences. Correctness of the individual code paths is pinned independently by the Class 1 analytical fixtures (Tests 01–07, 11, 13). +**Recommendation:** Trust SIMPLNX. Component sizes now retain their full width during classification. -> **Note — retired Test 08 A/B.** An earlier revision of this file cited a separate 7×7×3 three-way binary A/B run (DREAM3D 6.5.171 / a locally patched legacy build / DREAM3D-NX, `test_08_input.dream3d` + `fill_bad_data_vv/comparison_report.md`). Those working files were **not preserved and are unrecoverable**, so that comparison is **withdrawn** rather than left as an unverifiable citation. The SmallIN100 comparison above plus the analytical oracle fixtures are the standing evidence; if a second independent binary A/B is wanted for the record, it must be regenerated from scratch. +**Evidence:** PR #1653 (`a9d7e17e3`) removes the CCL size cast. The release sources through v7.4.0 use an Int32 cast on the BFS visit-list size; v7.4.1 uses an Int32 cast on the CCL component size. The fixtures do not reach this size. Release and legacy source checks are recorded in `../provenance/FillBadDataFilter.md`. -## Comparison method +--- -### SmallIN100 (automated, in-unit-test) +### FillBadDataFilter-D5 -| | | +| Field | Value | |---|---| -| **Comparison type** | SIMPLNX output vs `6_5_exemplar.dream3d` via `UnitTest::CompareExemplarToGeneratedData` | -| **Input** | `6_5_fill_bad_data/6_5_input.dream3d` (Small IN100 dataset) | -| **Parameters** | `MinAllowedDefectSize=1000`, `StoreAsNewPhase=false` | -| **Tolerance** | Element-wise equality, all cell-data arrays | -| **Configurations** | In-core only. `StoreAsNewPhase=false`. | -| **Result** | **Pass** | +| **Deviation ID** | `FillBadDataFilter-D5` | +| **Filter UUID** | `a59eb864-9e6b-40bb-9292-e5281b0b4f3e` | +| **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 large image could copy an invalid neighbor tuple during filling. + +**Root cause:** bug. The neighbor map stored indices as Int32. An index above INT32_MAX could become negative or wrap. Current BFS uses `int64` neighbor entries; CCL uses 64-bit candidate indices. DREAM.3D v6.5.171 stores its neighbor map in an Int32 array. + +**Affected users:** Users whose selected fill source has a linear index above 2,147,483,647. + +**Recommendation:** Trust SIMPLNX. The neighbor map now preserves 64-bit indices. + +**Evidence:** PR #1653 (`a9d7e17e3`) widens the neighbor map. The current CCL uses `FillPair` records. The fixtures do not reach this index range. Release and legacy source checks are recorded in `../provenance/FillBadDataFilter.md`. --- -## Non-deviations (documented for awareness) +## Part 2 — SIMPLNX defects and behavior changes -### Architectural difference: 4-phase CCL vs. legacy in-memory +### FillBadDataFilter-D6 -SIMPLNX uses chunk-sequential CCL with Union-Find (Phases 1–3) to identify connected bad-data regions before applying the size threshold. Legacy DREAM3D 6.5.171 used a simpler in-memory approach. For in-core datasets the functional output is identical; the CCL architecture is a strategy for OOC correctness, not a behavioral change. +| Field | Value | +|---|---| +| **Deviation ID** | `FillBadDataFilter-D6` | +| **Filter UUID** | `a59eb864-9e6b-40bb-9292-e5281b0b4f3e` | +| **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 | -**Evidence:** `FillBadData_SmallIN100` passes (SIMPLNX output matches the 6.5.x exemplar element-wise). +**Symptom:** FeatureIds from another geometry could be interpreted with the selected image dimensions. -### Tie-breaking in Phase 4 iterative fill +**Root cause:** bug. The image and FeatureIds parameters were selected independently, without checking that they belonged together. The filter now checks the resolved Cell Attribute Matrix and returns -16501 for an invalid selection. DREAM.3D v6.5.171 derives the geometry from the FeatureIds container, so it has no independent image-selection path. -When two features have equal neighbor vote counts, the first-encountered neighbor wins by linear scan order. Both SIMPLNX and legacy DREAM3D use the same linear order, so tie-break behavior is scan-order-equivalent. This is an algorithm characteristic, not a defect. +**Affected users:** Users who select FeatureIds outside the selected image geometry. -**Evidence:** `Test11_NeighborTieBreaking` passes against its analytically derived expected file. +**Recommendation:** Trust SIMPLNX. Select FeatureIds from the selected image geometry's Cell Attribute Matrix. + +**Evidence:** PR #1829 (`419f9db35`) adds the containment checks. `Strict Feature Ids` checks rejection even when the other Attribute Matrix has the same tuple shape. Separate checks reject modified Phases (-16502) and ignored arrays (-16503) outside the Cell Attribute Matrix. Release and legacy source checks are recorded in `../provenance/FillBadDataFilter.md`. + +--- + +### FillBadDataFilter-D7 + +| Field | Value | +|---|---| +| **Deviation ID** | `FillBadDataFilter-D7` | +| **Filter UUID** | `a59eb864-9e6b-40bb-9292-e5281b0b4f3e` | +| **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:** An array selected through an alias could be modified despite appearing in Attribute Arrays to Ignore. + +**Root cause:** bug. Array discovery used the geometry's Cell Attribute Matrix path. The ignored list could name the same array through another parent path, so path equality failed. The filter now normalizes FeatureIds and ignored paths before reporting and applying modifications. DREAM.3D v6.5.171 has no shared-parent alias path. + +**Affected users:** Users who access a Cell Attribute Matrix through more than one parent. + +**Recommendation:** Trust SIMPLNX. Ignored arrays now remain unchanged when selected through an alias. + +**Evidence:** PR #1829 (`419f9db35`) adds path normalization. `Ignored Array Cell AM Alias` checks unchanged sentinel values, changed FeatureIds and modification actions. Released `GetAllChildDataPaths()` compares paths rather than resolved object identities. Release and legacy source checks are recorded in `../provenance/FillBadDataFilter.md`. + +--- 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. From 92d6ddc3142f6ba8c654ab1aab2600e97b467993 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 20:01:29 -0400 Subject: [PATCH 55/91] DOC: Rewrite the IdentifySample V&V record to follow the report template Signed-off-by: Michael Jackson --- .../SimplnxCore/vv/IdentifySampleFilter.md | 171 +++++++++++------- .../vv/deviations/IdentifySampleFilter.md | 138 ++++++++++---- .../vv/provenance/IdentifySampleFilter.md | 150 +++++++-------- 3 files changed, 265 insertions(+), 194 deletions(-) diff --git a/src/Plugins/SimplnxCore/vv/IdentifySampleFilter.md b/src/Plugins/SimplnxCore/vv/IdentifySampleFilter.md index 4710100091..6051febbc7 100644 --- a/src/Plugins/SimplnxCore/vv/IdentifySampleFilter.md +++ b/src/Plugins/SimplnxCore/vv/IdentifySampleFilter.md @@ -1,101 +1,138 @@ # V&V Report: IdentifySampleFilter -| | | -|-----------|------------------| -| Plugin | SimplnxCore | +| | | +|--------|--------------| +| Plugin | SimplnxCore | | SIMPLNX UUID | `94d47495-5a89-4c7f-a0ee-5ff20e6bd273` | -| DREAM3D 6.5.171 equivalent | `IdentifySample` — UUID `0e8c0818-a3fb-57d4-a5c8-7cb8ae54a40a` | -| Verified commit | *pending* | -| Status | COMPLETE | -| Sign-off | Nathan Young, 06-17-2026 | +| SIMPLNX Human Name | Isolate Largest Feature (Identify Sample) | +| DREAM.3D v6.5.171 equivalent | `IdentifySample` (Processing plugin, UUID `0e8c0818-a3fb-57d4-a5c8-7cb8ae54a40a`) | +| Verified commit | ** | +| Status | READY FOR REVIEW | +| Sign-off | Nathan Young, 2026-06-17; second engineer: pending | ## At a glance -| Aspect | Current state | -|------------------------|--------------------------| -| Algorithm Relationship | Port for BFS; the OOC CCL implementation uses external equivalences and deterministic slice replay. Both implement largest-component selection and optional hole filling. | -| Oracle (confirmed) | Class 1 analytical masks for the non-square fixtures and the new HDF5 equivalence-page fixture. The historical archive retains its validated-circular classification. | -| Code paths enumerated | 16 of 19 exercised; 1D, single-voxel, and cancellation paths are not directly tested by this suite. | -| Tests today | 5 local registered test cases, 1 additional OOC parity case, and 1 hidden HDF5 equivalence-page and slice oracle, with hole filling off and on. | -| Exemplar archive | `identify_sample_v2.tar.gz` remains active. The new oracle is constructed in the test source. | -| Legacy comparison | Run — largest-component connectivity matches DREAM3D 6.5.171. Existing `IdentifySample-D1` documents the 2D/1D hole-fill difference. | -| Bug flags | The existing single-voxel SIMPLNX defect is corrected; the legacy hole-fill deviation remains documented. | -| V&V phase | The original COMPLETE status and sign-off are retained. OOC recertification adds a Class 1 page/slice-boundary fixture and paired CTest runs. | +| Aspect | Current state | +|--------|---------------| +| Algorithm Relationship | Rewrite of `IdentifySample` in DREAM.3D v6.5.171. BFS retains the two-pass search; CCL uses component labels and replay, and SIMPLNX adds slice processing. | +| Oracle (confirmed) | Class 1 masks in the three non-square cases in `test/IdentifySampleTest.cpp` check largest-component selection. All pass in CI. | +| Code paths enumerated | 21 of 44 exercised. Gaps include flat-image hole filling, ties, cancellation, and storage failures. | +| Tests today | 6 test cases: 1 archive case with 12 sections, 3 analytical cases, 1 conversion case, and 1 hidden HDF5 case. The five ordinary cases all pass in CI. | +| Exemplar archive | `identify_sample_v2.tar.gz` supplies inputs and SIMPLNX-generated regression outputs. Inline analytical masks provide the independent oracle. | +| Legacy comparison | Not run — comparison artifacts are not available. | +| Bug flags | `IdentifySampleFilter-D6` | +| V&V phase | Source review, analytical fixtures, and archive regression are complete. Second-engineer oracle review and a current legacy comparison remain open. | ## Summary -`IdentifySampleFilter` isolates the largest connected component of "good" voxels in an `ImageGeom`, clearing all smaller components. An optional `FillHoles` pass then closes interior holes — bad-voxel components that don't touch the volume boundary are flipped to good. `SliceBySlice` mode (new in SIMPLNX) applies the same two-pass logic independently per 2D slice. The algorithm is purely boolean/integer, so no floating-point precision drift is possible. The genuinely-3D path is a confirmed port of legacy `IdentifySample::execute()`, including an exact-match BFS `checked`-reset behavior between phases. A 2026-06-29 A/B run (stock 6.5.171 vs SIMPLNX, with root causes proven via a surgically patched local build of the legacy source) established the full degenerate-geometry story: **connectivity / largest-component selection is byte-identical** to legacy on 2D, 1D, and 3D inputs (including tie-breaks), so the one active deviation (`IdentifySample-D1`) is **not** in neighbor dispatch as originally hypothesized — it is that **legacy never fills holes on a 2D/1D geometry** (its hole-fill boundary test treats the flat dimension as always-boundary). The single-voxel hole-fill behavior, also originally suspected to be a legacy issue, turned out to be the reverse: a SIMPLNX defect, since resolved so that SIMPLNX matches legacy. +`IdentifySampleFilter` keeps the largest face-connected mask component and can fill enclosed holes, either for the whole image or for each selected slice. Class 1 masks check connectivity, and twelve archive sections check regression behavior. The fixed defects are listed below; CCL still leaves enclosed holes unfilled on flat images (`IdentifySampleFilter-D6`). ## Algorithm Relationship -**Port** of the 3D BFS two-pass algorithm and of connectivity on all geometries; **deviation** in hole-fill on degenerate (2D/1D) geometry; **new** SliceBySlice mode. +*Classification:* Rewrite -- **3D mode**: identical two-pass structure (largest-component isolation, then optional hole fill) to legacy. The `checked`-tracker reset between phases — a historically fragile point — is confirmed correct and identical in both versions. -- **Dimensionality dispatch** (`ProcessVoxels<>`): routes to `Image3D`, `EmptyZ/Y/XImage2D`, `Z/Y/XImage1D`, or `SingleVoxelImage` neighbor templates based on which dims equal 1. Legacy used a single fixed 3D-stride formula with coordinate-based guards. The A/B run (2026-06-29) confirmed these produce **byte-identical connectivity** on 2D/1D/3D — so the dispatch is a faithful re-implementation, **not** the source of the deviation. The actual deviation (`IdentifySample-D1`) is in the **hole-fill boundary test**, where legacy fails on 2D/1D; see deviations file. -- **New, no legacy path to compare against**: `SliceBySlice` mode (gated behind a parameter absent from DREAM3D 6.5.171 and unmapped by `FromSIMPLJson`); cancel checks (`m_ShouldCancel`); rate-limited progress messages. +*Evidence:* `SimplnxCoreLegacyUUIDMapping.hpp` maps the legacy filter to this filter; BFS retains the two-pass search, while `IdentifySampleCCL.cpp` replaces it with label equivalences and replay. ## Oracle -| Test group | Class | Basis | -|---|---|---| -| 3 non-square 2D tests | **1 (Analytical)** | 12-voxel hand-built mask, expected output hard-coded; one fixture per `EmptyZ/Y/XImage2D` dispatch path | -| 12 archive tests (`identify_sample_v2.tar.gz`) | **Validated-circular** | Exemplars generated by SIMPLNX's own filter (confirmed via `Generate_Exemplars.py`) over a hand-designed, human-reviewed `16×14×12` feature layout. Stronger than bare-circular (documented features, human review) but **not** Class 2 — no legacy parity claim possible. None of the 12 exercise degenerate geometry, so they provide no coverage for `IdentifySample-D1`. | +*Class:* 1 (Analytical). -Full provenance detail, including the feature-layout table and the resolved "checked-reset" question, in `vv/provenance/IdentifySampleFilter.md`. +*Applied:* A hand-built 3×4 mask has components of four and two cells; only indices 0, 1, 3, and 4 must remain true. + +*Encoded:* `src/Plugins/SimplnxCore/test/IdentifySampleTest.cpp::SimplnxCore::IdentifySampleFilter: 2D Empty Z Non-Square {3,4,1}` and the Empty Y and Empty X companion cases encode three rotated fixtures. All pass in CI. + +*Second-engineer review:* Pending. ## Bugs found and fixed -The existing single-voxel hole-fill correction is retained. Released DREAM3D-NX versions through v7.4.1 could fill a lone bad voxel; DREAM.3D 6.5.171 correctly left it unchanged. The deviation sidecar documents this historical fix but does not assign it a stable deviation ID. This recertification does not create a new defect or change that disposition. +All defects in this table are fixed. DREAM3D-NX v7.4.2 contains the BFS fix for `IdentifySample-D1` and the fixes for `IdentifySampleFilter-D2` and `IdentifySampleFilter-D3`. DREAM3D-NX v7.4.1 contains the fixes for `IdentifySampleFilter-D4` and `IdentifySampleFilter-D5`. -## Code path coverage +| Deviation | Defect | Affected released versions | Resolution | +|-----------|--------|----------------------------|------------| +| `IdentifySample-D1` | Whole-image hole filling treated a flat axis as a boundary at every cell. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | BFS ignores flat axes when it checks enclosure. The separate CCL issue is `IdentifySampleFilter-D6`. | +| `IdentifySampleFilter-D2` | The dimensional BFS filled a lone false voxel because its neighbor loop was empty. | None. DREAM3D-NX v7.0.0 through v7.4.2 were not affected. DREAM.3D v6.5.171 was not affected. | A single voxel starts with its boundary flag set and stays false. | +| `IdentifySampleFilter-D3` | Slice processing kept the first of two equally large components, unlike whole-image BFS. | DREAM3D-NX v7.0.2 through v7.4.1. DREAM.3D v6.5.171 was not affected. | The slice BFS uses `>=` and keeps the later component on a tie. | +| `IdentifySampleFilter-D4` | Slice hole filling used a local plane index as a global index for XZ and YZ planes. | DREAM3D-NX v7.0.2 through v7.4.0. DREAM.3D v6.5.171 was not affected. | Plane coordinates and strides place each filled voxel in the selected slice. | +| `IdentifySampleFilter-D5` | Slice hole filling reused visited flags from component selection and skipped removed internal islands. | DREAM3D-NX v7.0.2 through v7.4.0. DREAM.3D v6.5.171 was not affected. | Slice BFS clears its visited flags before hole filling. CCL labels the false components in a separate pass. | -16 of 19 paths exercised. Source: `Algorithms/IdentifySample.cpp` (28 lines), `IdentifySampleBFS.cpp` (296 lines), and `IdentifySampleCCL.cpp` (1,052 lines). - -| # | Path | Exercised by | -|---|---|---| -| 1 | Dispatch: `SliceBySlice=false` → `ProcessVoxels` | `whole_*` + 3 non-square 2D tests | -| 2 | Dispatch: `SliceBySlice=true` → slice functor | `sliced_*` | -| 3 | Dim dispatch: `emptyDimCount==0` → `Image3D` | `whole_*` | -| 4–6 | Dim dispatch: `emptyDimCount==1` → `EmptyZ/Y/XImage2D` | 3 non-square 2D tests (one each) | -| 7 | Dim dispatch: `emptyDimCount==2` → 1D variants | *Not tested. Impractical/unused in practice; gap acceptable.* | -| 8 | Dim dispatch: `emptyDimCount==3` → `SingleVoxelImage` | *Not tested. Degenerate case; gap acceptable.* | -| 9–11 | BFS expansion, valid-neighbor add, boundary skip | All BFS tests | -| 12 | Clear non-largest component | `whole_*` + 3 non-square 2D tests | -| 13 | `FillHoles=false` → hole-fill skipped | `*_nofill` | -| 14 | `FillHoles=true`, enclosed → filled | `*_fill` | -| 15 | `FillHoles=true`, touches boundary → stays bad | `*_fill` | -| 16 | Cancel mid-scan → early return | *Not tested. Cancel disregard would hang any test; low-value gap.* | -| 17 | Slice plane selection (XY/XZ/YZ) | `sliced_xy/xz/yz_*` | -| 18 | External equivalences cross the 4,096-record page boundary | `genuine HDF5 equivalence-page and slice oracle` — over 8,000 provisional labels merge into the largest component | -| 19 | CCL rolling-slice labeling and replay, with optional hole fill | `genuine HDF5 equivalence-page and slice oracle` — every output mask value is checked, and runtime counters prove CCL runs on HDF5 storage | +## Code path coverage +21 of 44 paths exercised. The table separates test assertions from paths that have no direct fixture. + +Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/IdentifySample.cpp` (28 lines); `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/IdentifySampleBFS.cpp` (295 lines); `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/IdentifySampleCCL.cpp` (1055 lines); `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/IdentifySampleCommon.hpp` (517 lines); `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/IdentifySampleFilter.cpp` (139 lines). + +`Archive` below means `SimplnxCore::IdentifySampleFilter`. Other names omit the `SimplnxCore::IdentifySampleFilter: ` prefix. The ordinary mask cases select BFS and CCL on resident stores through the scenario harness. + +| # | Phase | Path | Test case | +|---|-------|------|-----------| +| 1 | BFS dispatch | Whole image, uint8 mask, all dimensions greater than one. | Archive — `whole_*`. | +| 2 | BFS dispatch | Boolean mask. | All three `2D Empty * Non-Square` cases. | +| 3 | BFS dispatch | Flat Z dimension. | `2D Empty Z Non-Square {3,4,1}`. | +| 4 | BFS dispatch | Flat Y dimension. | `2D Empty Y Non-Square {3,1,4}`. | +| 5 | BFS dispatch | Flat X dimension. | `2D Empty X Non-Square {1,3,4}`. | +| 6 | BFS dispatch | X-only line. | *Not directly tested. No one-dimensional fixture exists.* | +| 7 | BFS dispatch | Y-only line. | *Not directly tested. No one-dimensional fixture exists.* | +| 8 | BFS dispatch | Z-only line. | *Not directly tested. No one-dimensional fixture exists.* | +| 9 | BFS dispatch | Single voxel, including its hole-fill boundary guard. | *Not directly tested. No single-voxel fixture exists.* | +| 10 | BFS selection | Expand through valid true neighbors; skip invalid, false, and visited neighbors. | Archive; all three non-square cases. | +| 11 | BFS selection | Keep the largest component and clear smaller components. | Archive; all three non-square cases. | +| 12 | BFS filling | `FillHoles=false`. | Archive — `whole_*_nofill`; all three non-square cases. | +| 13 | BFS filling | Fill an enclosed 3D false component after resetting visited flags. | Archive — `whole_*_fill`, including the internal island. | +| 14 | BFS filling | Leave a boundary-connected false component unchanged. | Archive — `whole_*_fill`. | +| 15 | BFS filling | Fill an enclosed hole in a flat image. | *Not directly tested. The non-square fixtures disable hole filling.* | +| 16 | BFS selection | Equal-size whole-image components select the later component. | *Not directly tested. No fixture isolates an equal-size largest-component tie.* | +| 17 | Slice BFS | XY plane, filling disabled and enabled; contiguous plane transfers. | Archive — `sliced_xy_nofill`, `sliced_xy_fill`. | +| 18 | Slice BFS | XZ plane, filling disabled and enabled; contiguous row transfers. | Archive — `sliced_xz_nofill`, `sliced_xz_fill`. | +| 19 | Slice BFS | uint8 YZ plane, filling disabled and enabled; column batch transfers. | Archive — `sliced_yz_nofill`, `sliced_yz_fill`. | +| 20 | Slice BFS | Boolean YZ plane uses scalar transfers. | *Not directly tested. Slice fixtures use uint8 masks.* | +| 21 | Slice BFS | A full 64-column YZ batch followed by another batch. | *Not directly tested. The archive has only 16 X cells.* | +| 22 | Slice BFS | Reset visited flags and fill a removed internal island. | Archive — `sliced_*_fill`. | +| 23 | Slice BFS | Equal-size components select the later component. | *Not directly tested. No fixture isolates an equal-size largest-component tie.* | +| 24 | CCL dispatch | Whole-image labeling on resident bool and uint8 masks. | Archive — `whole_*`; all three non-square cases. | +| 25 | Slice CCL | XY planes with filling disabled and enabled. | Archive — `sliced_xy_nofill`, `sliced_xy_fill`. | +| 26 | Slice CCL | XZ planes with filling disabled and enabled. | Archive — `sliced_xz_nofill`, `sliced_xz_fill`. | +| 27 | Slice CCL | YZ planes with filling disabled and enabled. | Archive — `sliced_yz_nofill`, `sliced_yz_fill`. | +| 28 | CCL selection | Whole image has no true component; return without replay. | *Not directly tested. No all-false whole-image fixture exists.* | +| 29 | Slice CCL | An empty plane returns without component removal. | Archive — slices outside all documented features. | +| 30 | CCL selection | Create labels, merge connected labels, and replay to remove smaller roots. | Archive; all three non-square cases. | +| 31 | CCL filling | Label false components, mark boundary roots, and fill enclosed roots. | Archive — `whole_*_fill` and `sliced_*_fill`. | +| 32 | CCL filling | A flat whole image marks every false root as boundary-connected. | *Not directly tested. Flat fixtures disable hole filling; see `IdentifySampleFilter-D6`.* | +| 33 | CCL storage | HDF5 mask and equivalences cross a 4096-record page boundary. | *Not directly tested. The hidden `genuine HDF5 equivalence-page and slice oracle` requires a real HDF5 store; the supplied CI evidence covers in-core builds.* | +| 34 | CCL storage | A failed temporary-store request falls back to resident records. | *Not directly tested. No fixture isolates provider failure and fallback.* | +| 35 | CCL storage | Provider failure without fallback, or null store (`-45460`). | *Not directly tested. No fixture injects these provider responses.* | +| 36 | CCL storage | Maximum label count overflows the sentinel slot (`-45461`). | *Not directly tested. Requires an image count of UINT64_MAX.* | +| 37 | CCL storage | Boundary-root cache allocation fails (`-45462`). | *Not directly tested. No fixture injects allocation failure.* | +| 38 | Storage | Mask reads or writes fail in BFS slice transfers or CCL scans and replay. | *Not directly tested. No mask store injects an I/O failure.* | +| 39 | CCL storage | Equivalence or root-flag creation, access, or flush fails. | *Not directly tested. No temporary store injects these failures.* | +| 40 | Cancellation | Return during BFS scanning, slice processing, CCL labeling, or replay. | *Not directly tested. No fixture injects cancellation.* | +| 41 | CCL selection | Equal-size roots select the larger root label. | *Not directly tested. No fixture isolates a tie between largest roots.* | +| 42 | CCL dispatch | One-dimensional or single-voxel whole image. | *Not directly tested. The smallest flat fixture is a 3×4 plane.* | +| 43 | Slice CCL | Boolean masks in XY, XZ, or YZ planes. | *Not directly tested. All slice fixtures use uint8 masks.* | +| 44 | Slice BFS | Boolean masks in XY or XZ planes. | *Not directly tested. All slice fixtures use uint8 masks.* | ## Test inventory +The five ordinary cases in `src/Plugins/SimplnxCore/test/IdentifySampleTest.cpp` run in the supplied in-core CI configurations. All pass in CI. + | Test case | Status | Notes | |-----------|--------|-------| -| `IdentifySampleFilter` | kept | Twelve archive sections cover whole/sliced, XY/XZ/YZ, and hole filling. Runtime witnesses exercise BFS and CCL on resident stores. | -| `SIMPL Backwards Compatibility` | kept | SIMPL 6.4 and 6.5 conversion. | -| `2D Empty Z Non-Square {3,4,1}` | kept | Twelve-cell analytical largest-component mask. | -| `2D Empty Y Non-Square {3,1,4}` | kept | The same independent mask in the XZ plane. | -| `2D Empty X Non-Square {1,3,4}` | kept | The same independent mask in the YZ plane. | -| `genuine HDF5 equivalence-page and slice oracle` | new-for-V&V | A 129×129×3 mask creates over 8,000 labels before the second slice merges them. The test checks every mask byte, disconnected removal, and an enclosed hole with filling off/on. | - -The new fixture requires HDF5-OOC and observes the OOC-on-OOC dispatch counter. The first slice contains isolated checkerboard voxels; the next slice joins the component at x<127. Components at x=128 stay disconnected and must be removed. A small bridge above the interior hole keeps its upper voxel in the main component. Expected masks come from this geometry, independently of the CCL implementation. The existing upstream/develop analytical assertions and archive comparisons are retained. - -OOC recertification, 2026-09-18: serial CTest passed 5/5 in `NX-Com-Qt69-Vtk96-Rel` and 6/6 in `NX-Com-Qt69-Vtk96-OoC-Rel`. The new hidden boundary case passed 99,875 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. +| `SimplnxCore::IdentifySampleFilter` | kept | Compares every uint8 mask value with `Mask Exemplar` in 12 archive sections: whole/sliced × XY/XZ/YZ × fill/nofill. Checks tuple dimensions. | +| `SimplnxCore::IdentifySampleFilter: genuine HDF5 equivalence-page and slice oracle` | new-for-V&V | Hidden. Builds a 129×129×3 mask and checks every value with filling off/on, removal of disconnected components, and the CCL dispatch counter. | +| `SimplnxCore::IdentifySampleFilter: SIMPL Backwards Compatibility` | kept | Dynamic variants `SIMPL 6.5 (UUID)` and `SIMPL 6.4 (Filter_Name)` check identity, comments, and three converted arguments. | +| `SimplnxCore::IdentifySampleFilter: 2D Empty Z Non-Square {3,4,1}` | kept | Checks all 12 bool values against the four-cell analytical component in the XY plane. | +| `SimplnxCore::IdentifySampleFilter: 2D Empty Y Non-Square {3,1,4}` | kept | Checks the same analytical component in the XZ plane. | +| `SimplnxCore::IdentifySampleFilter: 2D Empty X Non-Square {1,3,4}` | kept | Checks the same analytical component in the YZ plane. | ## Exemplar archive -- **Active:** `identify_sample_v2.tar.gz` — SHA512 `a7ffac3e...848ac31`. Provenance: `vv/provenance/IdentifySampleFilter.md`. -- **Orphaned:** `identify_sample.tar.gz` (v1) — no active test references it; recommend removing its `download_test_data()` entry from `CMakeLists.txt`. - -## Deviations from DREAM3D 6.5.171 - -**3D mode + connectivity:** No deviations — confirmed identical two-pass BFS structure (including the `checked`-reset between phases), and A/B-verified (2026-06-29) byte-identical largest-component output on 2D/1D/3D inputs and tie-breaks. +- **Archive:** `identify_sample_v2.tar.gz` +- **SHA512:** `a7ffac3eaad479c07215c1dd16274c45a52466708a9d27b5f85a29b0eba3b6705b627e1052a7a27e9bfe89cd6e7df673beb7a1e98b262b6c52ea383b4848ac31` +- **Provenance:** `src/Plugins/SimplnxCore/vv/provenance/IdentifySampleFilter.md` -**`IdentifySample-D1` (active, A/B-proven):** Hole fill (`FillHoles=true`) on 2D/1D geometry. Legacy's hole-fill boundary test includes `plane==0 || plane==(zp-1)`, which is always true when `zp==1`, so legacy flags every voxel as boundary-touching and **never fills holes in a 2D/1D geometry**. SIMPLNX fills enclosed holes correctly. Proven by A/B (legacy left a 2D enclosed hole unfilled; SIMPLNX filled it) and by a dimensionality-aware boundary-test fix applied to a local build of the legacy source, which then reproduces SIMPLNX. Recommend trusting SIMPLNX. Full entry in `vv/deviations/IdentifySampleFilter.md`. +## Deviations from DREAM.3D v6.5.171 -**Single-voxel hole fill:** Not a legacy deviation. Legacy correctly leaves a lone bad voxel unfilled; released versions through 7.4.1 wrongly fill it (empty neighbor loop left `touchesBoundary=false`). This PR's `touchesBoundary = k_NeighborCount == 0` (`IdentifySample.cpp:139`) fixes SIMPLNX to match legacy; verified by A/B. Full entry in `vv/deviations/IdentifySampleFilter.md`. +See `vv/deviations/IdentifySampleFilter.md` for the root cause, affected users, and recommendation. Part 1 contains the following source-confirmed difference. -**`SliceBySlice` and cancel checks:** Genuinely new, no legacy path to deviate from. +| Deviation | Observed difference | +|-----------|---------------------| +| `IdentifySample-D1` | Whole-image BFS fills enclosed holes on 2D images; DREAM.3D v6.5.171 treats every cell on a flat axis as a boundary cell. | diff --git a/src/Plugins/SimplnxCore/vv/deviations/IdentifySampleFilter.md b/src/Plugins/SimplnxCore/vv/deviations/IdentifySampleFilter.md index 2a28844eb5..ca34e9a8c3 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/IdentifySampleFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/IdentifySampleFilter.md @@ -1,77 +1,137 @@ -# Deviations from DREAM3D 6.5.171: IdentifySample +# Deviations from DREAM.3D v6.5.171: IdentifySampleFilter -This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent. +Part 1 records the difference from DREAM.3D v6.5.171. Part 2 records SIMPLNX defects and behavior changes. The existing `IdentifySample-D1` ID is unchanged; new entries continue the sequence as `IdentifySampleFilter-D2` through `IdentifySampleFilter-D6`. The Filter UUID is the permanent cross-reference anchor. -Entries are referenced by stable ID (`IdentifySample-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 + +### IdentifySample-D1 + +| Field | Value | +|---|---| +| **Deviation ID** | `IdentifySample-D1` | +| **Filter UUID** | `94d47495-5a89-4c7f-a0ee-5ff20e6bd273` | +| **Status** | active | + +**Symptom:** With `FillHoles=true` and whole-image processing, BFS fills an enclosed hole on a 2D image. DREAM.3D v6.5.171 leaves it false. + +**Root cause:** Bug. DREAM.3D v6.5.171 checks the extrema of all three axes when it tests whether a false component reaches the boundary. A unit dimension makes that condition true at every cell. BFS checks neighbors only along non-flat axes. This corrects the enclosure test without changing face connectivity. DREAM3D-NX v7.0.0 through v7.4.1 used the same boundary test as DREAM.3D v6.5.171; DREAM3D-NX v7.4.2 contains the BFS correction. The separate CCL behavior is `IdentifySampleFilter-D6`. + +**Affected users:** Users who fill enclosed holes on a single-slice image with whole-image BFS. DREAM.3D v6.5.171 was affected. The legacy boundary test also marks every cell on a one-dimensional image as boundary-connected. + +**Recommendation:** Trust SIMPLNX BFS for 2D hole filling. The flat axis must not make an otherwise enclosed hole part of the exterior. + +**Evidence:** DREAM.3D tag `v6.5.171`, `Source/Plugins/Processing/ProcessingFilters/IdentifySample.cpp`, uses the three-axis coordinate test. Current `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/IdentifySampleBFS.cpp` selects `EmptyZImage2D`, `EmptyYImage2D`, or `EmptyXImage2D` and tests valid neighbors. PR #1590 (`e76ad508e`) introduced this dimensional dispatch. Direct inspection of all `upstream/release/v7.0.0` through `upstream/release/v7.4.2` sources establishes the release range. The three non-square tests check connectivity only; they disable hole filling. --- -## 3D mode — no deviations +## Part 2 — SIMPLNX defects and behavior changes -Source-inspection comparison of the genuinely-3D path (`IdentifySampleFunctor`) against DREAM3D 6.5.171 `IdentifySample::execute()` confirmed identical algorithmic structure: +### IdentifySampleFilter-D2 -- **Phase 1 (largest-component isolation):** BFS flood-fill over all good voxels; tracks the largest connected component; all good voxels outside that component are set to false/0. -- **Phase 2 (hole fill, optional):** BFS flood-fill over all bad voxels; connected components that do not reach the volume boundary are flipped to good. +| Field | Value | +|---|---| +| **Deviation ID** | `IdentifySampleFilter-D2` | +| **Filter UUID** | `94d47495-5a89-4c7f-a0ee-5ff20e6bd273` | +| **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 | -The algorithm is purely boolean/integer — no floating-point arithmetic — so precision drift cannot occur between two correct implementations of the same 3D logic. +**Symptom:** Dimensional BFS could change the only false voxel in a 1×1×1 image to true when hole filling was enabled. -**Confirmed parity — `checked` reset between phases:** `vv/provenance/IdentifySampleFilter.md` documents an "Internal cluster" fixture in `identify_sample_v2.tar.gz`, purpose-built to catch a bug where the `checked` BFS-visited tracker is not reset between Phase 1 and Phase 2 — voxels visited (and marked `checked=true`) while being absorbed into a non-largest component during Phase 1 would then be skipped entirely during Phase 2's hole-fill scan, since `checked` would already be `true` for them, leaving them permanently un-filled even when they qualify as an enclosed hole. +**Root cause:** Bug. `SingleVoxelImage` has zero neighbors. The empty neighbor loop left `touchesBoundary=false`. The corrected initialization, `touchesBoundary = k_NeighborCount == 0`, treats the voxel as boundary-connected. Earlier released implementations used coordinate checks and already left this voxel false. -Direct source comparison confirms **both versions reset correctly and this is not a deviation**: +**Affected users:** Users of the dimensional BFS before its single-voxel correction. No listed DREAM3D-NX release exposed the defect. -- **Legacy** (`DREAM3D/Source/Plugins/Processing/ProcessingFilters/IdentifySample.cpp:254`, checked out at tag `v6.5.171`): `checked.assign(totalPoints, false);` immediately before the `m_FillHoles` block. -- **SIMPLNX 3D mode** (`Algorithms/IdentifySample.cpp:112`): `checked.assign(totalPoints, false);` immediately before the `if(fillHoles)` block. -- **SIMPLNX SliceBySlice mode** (`Algorithms/IdentifySample.cpp:325`): `checked.assign(planeDim1 * planeDim2, false);` immediately before its own `if(fillHoles)` block, scoped per-slice. +**Recommendation:** Trust SIMPLNX. A single false voxel stays false because it touches the image boundary. -The bug referenced in the test data's `generated_data/ReadMe.md` was a SIMPLNX-only regression at some prior point in development (already fixed in current source, as shown above) — it never existed in DREAM3D 6.5.171. The "Internal cluster" fixture remains in the archive as a guard against reintroducing it, not as evidence of any current or legacy deviation. +**Evidence:** PR #1640 (`a332e7369`) adds the guard after PR #1590 introduced dimensional dispatch. Releases through v7.4.1 have coordinate boundary checks; v7.4.2 has the guard. DREAM.3D v6.5.171 also has coordinate checks. Current `IdentifySampleBFS.cpp` retains the guard; no test in `IdentifySampleTest.cpp` constructs a single-voxel image. --- -## IdentifySample-D1 +### IdentifySampleFilter-D3 | Field | Value | |---|---| -| **Deviation ID** | `IdentifySample-D1` | +| **Deviation ID** | `IdentifySampleFilter-D3` | | **Filter UUID** | `94d47495-5a89-4c7f-a0ee-5ff20e6bd273` | -| **Affected output** | hole fill (`FillHoles=true`) on 2D / 1D `ImageGeom` | -| **Status** | Active — **proven by A/B run (2026-06-29)**; legacy is incorrect, SIMPLNX is correct | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.2 through v7.4.1. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.2 | -**Symptom:** With `FillHoles=true` on a 2D or 1D `ImageGeom` (any dimension == 1), legacy `IdentifySample` never fills enclosed holes — every interior bad component is left bad. SIMPLNX correctly fills enclosed holes. Connectivity / largest-component selection (Phase 1) is **not** affected (verified identical — see below). +**Symptom:** Slice BFS selected the first of two equally large components, while whole-image BFS selected the later component. -**Root cause — the hole-fill boundary test, NOT neighbor dispatch.** This corrects the original (source-inspection) hypothesis that the deviation lived in neighbor-offset dispatch; the A/B run showed connectivity is identical and isolated the difference to the Phase-2 boundary test. Legacy's hole-fill boundary check (`Processing/ProcessingFilters/IdentifySample.cpp:287`) is: +**Root cause:** Behavior change. Slice selection used `currentVList.size() > biggestBlock`. Whole-image selection used `>=`. Slice BFS now also uses `>=`, so a later equal-size component replaces the earlier component. -```cpp -if(column==0 || column==(xp-1) || row==0 || row==(yp-1) || plane==0 || plane==(zp-1)) - touchesBoundary = true; -``` +**Affected users:** Users of slice processing with two or more components tied for largest size. DREAM.3D v6.5.171 has no slice-processing mode. -For a 2D geometry `zp==1`, so `plane==0` (and `plane==(zp-1)`) is unconditionally true — **every** voxel is flagged as touching the sample boundary, so no bad component is ever "enclosed" and no hole is filled. The same applies to 1D (two degenerate axes). SIMPLNX's `EmptyZ/Y/XImage2D` (and 1D) hole-fill flags the boundary only when an in-plane neighbor is actually invalid, so a genuinely interior hole is filled. +**Recommendation:** Trust SIMPLNX. Do not depend on the older first-component tie rule when comparing saved masks. -**A/B verification (2026-06-29):** degenerate fixtures were authored as legacy `.dream3d` and run through stock 6.5.171, SIMPLNX, and a local build of the legacy source with a surgical fix applied: +**Evidence:** PR #1640 (`a332e7369`) changes `>` to `>=`. Release sources v7.0.0 and v7.0.1 have no slice functor; v7.0.2 through v7.4.1 use `>`; v7.4.2 uses `>=`. Current `IdentifySampleCommon.hpp::processSlice()` retains that comparison. No current fixture isolates an equal-size largest-component tie. -- 2D `{5,5,1}` with one enclosed bad voxel, `FillHoles=true`: **stock 6.5.171 left it bad; SIMPLNX filled it.** -- Connectivity-only cases (2D `{3,4,1}` / `{1,3,4}` no-fill, 1D `{5,1,1}` with a tie-break, 3D `{4,4,4}` fill) were **byte-identical** across all three — confirming the deviation is isolated to hole-fill on degenerate geometry, and that the `>=` tie-break matches legacy. -- The **surgical fix to the local legacy build** (boundary test made dimensionality-aware: only extremes of non-degenerate dims count; an all-degenerate geometry is treated as boundary) made legacy fill the 2D hole **identically to SIMPLNX**, while leaving 3D and connectivity byte-for-byte unchanged. This pins the root cause to the boundary test. +--- -**Affected users:** Anyone running `IdentifySampleFilter` with `FillHoles=true` on a 2D or 1D `ImageGeom` (single-slice EBSD scan, line scan) expecting parity with DREAM3D 6.5.171 — legacy silently leaves holes unfilled. +### IdentifySampleFilter-D4 -**Recommendation:** Trust SIMPLNX. Legacy cannot fill holes in degenerate geometries — a genuine, now-proven bug. +| Field | Value | +|---|---| +| **Deviation ID** | `IdentifySampleFilter-D4` | +| **Filter UUID** | `94d47495-5a89-4c7f-a0ee-5ff20e6bd273` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.2 through v7.4.0. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.1 | + +**Symptom:** Hole filling in an XZ or YZ slice could write true values to the wrong image cells. + +**Root cause:** Bug. The write used `fixedIdx * fixedStride + idx`, where `idx` was a local plane index. The correction decomposed that index into two plane coordinates and applied their strides. Current slice implementations transfer the selected plane to a buffer and write it back with the correct image layout. + +**Affected users:** Users who enabled hole filling and selected XZ or YZ slice processing. DREAM.3D v6.5.171 has no slice-processing mode. + +**Recommendation:** Trust SIMPLNX. Recalculate masks from affected releases when slice hole filling used XZ or YZ. + +**Evidence:** PR #1562 (`42bc9ba6e`) corrects the index and registers `identify_sample_v2.tar.gz`. Release sources v7.0.2 through v7.4.0 contain the incorrect write; v7.4.1 and v7.4.2 contain the stride correction. `SimplnxCore::IdentifySampleFilter` compares all values in `sliced_xz_fill` and `sliced_yz_fill`. Current implementations are `IdentifySampleCommon.hpp` and `IdentifySampleCCL.cpp`. --- -## Single-voxel hole fill — a SIMPLNX bug fixed to match legacy (NOT a legacy deviation) +### IdentifySampleFilter-D5 + +| Field | Value | +|---|---| +| **Deviation ID** | `IdentifySampleFilter-D5` | +| **Filter UUID** | `94d47495-5a89-4c7f-a0ee-5ff20e6bd273` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.2 through v7.4.0. DREAM.3D v6.5.171 was not affected. | +| **Fixed in** | DREAM3D-NX v7.4.1 | -This was originally suspected (alongside `IdentifySample-D1`) to be a legacy deficiency. The A/B run showed the opposite. For a single-voxel `{1,1,1}` `ImageGeom` with `FillHoles=true`, **legacy correctly does not fill the lone bad voxel** (its coordinate boundary test flags it as boundary). The pre-fix SIMPLNX main functor left `touchesBoundary=false` because the neighbor loop never executes when `k_NeighborCount==0` (`SingleVoxelImage`), so it wrongly filled the voxel. +**Symptom:** Slice processing could leave false cells inside an enclosed hole where it had removed a smaller true island. -The current one-line form — `bool touchesBoundary = k_NeighborCount == 0;` (`Algorithms/IdentifySample.cpp:139`) — initializes the flag to `true` for `SingleVoxelImage`, fixing SIMPLNX to match legacy. Verified by A/B: after the fix, SIMPLNX and stock 6.5.171 both leave the single bad voxel unfilled. This is a SIMPLNX correctness fix, not a deviation from DREAM3D 6.5.171. +**Root cause:** Bug. Slice BFS reused the visited flags from largest-component selection. Hole filling skipped cells that the first pass had visited and then cleared. Slice BFS now resets the flags before hole filling. CCL labels false components in a separate pass. The legacy whole-image algorithm already reset its visited flags. + +**Affected users:** Users who enabled slice hole filling on masks with disconnected true islands inside an enclosed hole. DREAM.3D v6.5.171 has no slice-processing mode. + +**Recommendation:** Trust SIMPLNX. Recalculate affected slice masks to fill the removed internal islands. + +**Evidence:** PR #1562 (`42bc9ba6e`) adds `checked.assign(planeDim1 * planeDim2, false)`. Release sources v7.0.2 through v7.4.0 omit the slice reset; v7.4.1 and v7.4.2 contain it. The archive's `generated_data/ReadMe.md` defines an eight-cell internal island for this regression. The `sliced_*_fill` sections compare the complete output masks. Current `IdentifySampleCommon.hpp::processSlice()` clears the visited flags. --- -## New features (no legacy code path to compare against — not deviations) +### IdentifySampleFilter-D6 + +| Field | Value | +|---|---| +| **Deviation ID** | `IdentifySampleFilter-D6` | +| **Filter UUID** | `94d47495-5a89-4c7f-a0ee-5ff20e6bd273` | +| **Status** | active | +| **Affected released versions** | None. DREAM3D-NX v7.0.0 through v7.4.2 do not contain CCL. DREAM.3D v6.5.171 was affected by the same boundary rule. | +| **Fixed in** | Not fixed | + +**Symptom:** With whole-image processing and hole filling enabled, CCL leaves enclosed holes false on a 2D image. BFS fills them. + +**Root cause:** Bug. CCL marks a false component as exterior when any voxel lies at an extremum of any of the three axes. A flat axis therefore marks every false component as exterior. BFS excludes flat axes from its neighbor boundary check. This CCL regression restores the boundary rule documented in `IdentifySample-D1`. -These differ from `IdentifySample-D1` in that no input could have triggered the corresponding legacy behavior at all — there is nothing to deviate from. +**Affected users:** Users whose whole-image execution selects CCL for a flat image with an enclosed hole. Slice processing uses a separate boundary test. -- **`SliceBySlice` mode** (`IdentifySampleSliceBySliceFunctor`): gated behind a parameter (`SliceBySlice`) that does not exist in DREAM3D 6.5.171 and is not mapped by `FromSIMPLJson`. No legacy pipeline could have exercised this path. -- **Cancel checks** (`m_ShouldCancel`): UX-only; no algorithmic effect on a run that completes. +**Recommendation:** Use BFS for whole-image 2D hole filling, or select slice processing in the non-flat plane. Do not assume whole-image BFS and CCL give the same filled mask on a flat image. -*If a future run against DREAM3D 6.5.171 reveals additional deviations, add them here as `IdentifySample-D2` etc.* +**Evidence:** Current `IdentifySampleCCL.cpp` marks boundary roots with `x == 0 || x == dimX - 1 || y == 0 || y == dimY - 1 || z == 0 || z == dimZ - 1`. The behavior follows directly for a 5×5×1 true mask with a false center: `z == 0` marks that center as exterior. The filter dispatcher does not exclude flat images from CCL. PR #1568 (`4ec21e93f`) adds CCL after v7.4.2. All listed release sources contain only the earlier algorithm. The non-square tests disable hole filling, and the hidden HDF5 test uses a 129×129×3 image, so neither detects this case. 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. From bcf361f527b009d0e815afeb26f84ef6c280ec85 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 20:01:29 -0400 Subject: [PATCH 56/91] DOC: Rewrite the MultiThresholdObjects V&V record to follow the report template Signed-off-by: Michael Jackson --- .../vv/MultiThresholdObjectsFilter.md | 268 +++++++++--------- .../deviations/MultiThresholdObjectsFilter.md | 173 ++++------- 2 files changed, 187 insertions(+), 254 deletions(-) diff --git a/src/Plugins/SimplnxCore/vv/MultiThresholdObjectsFilter.md b/src/Plugins/SimplnxCore/vv/MultiThresholdObjectsFilter.md index 1e6bd0c5e8..47e85af4d5 100644 --- a/src/Plugins/SimplnxCore/vv/MultiThresholdObjectsFilter.md +++ b/src/Plugins/SimplnxCore/vv/MultiThresholdObjectsFilter.md @@ -1,176 +1,166 @@ # V&V Report: MultiThresholdObjectsFilter -| | | -|-----------|------------------| -| Plugin | SimplnxCore | +| | | +|--------|--------------| +| Plugin | SimplnxCore | | SIMPLNX UUID | `4246245e-1011-4add-8436-0af6bed19228` | -| DREAM3D 6.5.171 equivalent | Two separate legacy filters, consolidated: **Threshold Objects** (`MultiThresholdObjects`, SIMPL UUID `014b7300-cf36-5ede-a751-5faf9b119dae`) and **Threshold Objects (Advanced)** (`MultiThresholdObjects2`, SIMPL UUID `686d5393-2b02-5c86-b887-dd81a8ae80f2`) — both mapped to this single filter's UUID in `SimplnxCoreLegacyUUIDMapping.hpp` (see Algorithm Relationship) | +| SIMPLNX Human Name | Multi-Threshold Objects | +| DREAM.3D v6.5.171 equivalent | `MultiThresholdObjects` (Threshold Objects) and `MultiThresholdObjects2` (Threshold Objects (Advanced)) | | Verified commit | ** | -| Status | COMPLETE | -| Sign-off | Matthew Marine (V&V author, PR #1688). Second engineer: Michael A. Jackson , 2026-08-19 (PR #1688 review). | +| Status | COMPLETE | +| Sign-off | Matthew Marine, 2026-07-24; second engineer: Michael Jackson, 2026-08-19 | ## At a glance -| Aspect | Current state | -|------------------------|--------------------------| -| Algorithm Relationship | **Rewrite.** Consolidates two independently-shipped legacy filters — **Threshold Objects** (flat, AND-only) and **Threshold Objects (Advanced)** (nested AND/OR sets) — into one SIMPLNX filter under one new UUID, unified around a single `ArrayThresholdSet` model. Not a line-by-line translation of either legacy source. | -| Oracle (confirmed) | **Class 1 (Analytical) — confirmed.** `expected[i] = COMPARISON(input[i], value)`, hand-combined via AND/OR/invert boolean algebra. Encoded across 14 `TEST_CASE`/`TEMPLATE_TEST_CASE` declarations in `MultiThresholdObjectsTest.cpp`, all pass. | -| Code paths enumerated | **26 of 27 exercised.** Row 13 (unreachable comparison-operator `else`-throw) is a permanent, acceptable gap and the only one remaining. Row 25 (a set mixing a leaf threshold with a nested set — the `MultiThresholdObjectsFilter-D1` trigger shape) is now covered by the `ArraySet 6` / `ArraySet 7` `SECTION`s of `Valid Threshold Sets`. | -| Tests today | **14 `TEST_CASE`/`TEMPLATE_TEST_CASE` declarations / 31 ctest entries**, all passing in both the in-core and out-of-core Release builds (12 single-entry TEST_CASEs + 2 `TEMPLATE_TEST_CASE`s instantiated over 9 and 10 types respectively). Exhaustive sweeps over comparison operator × invert × union operator × set nesting × mask `DataType` (10 types, plus boolean covered elsewhere) × source-array `DataType` (11 types) × custom TRUE/FALSE execution (10 types), plus negative/error-path groups and a SIMPL backwards-compatibility check. All fixtures built in-memory. | -| Exemplar archive | **None.** All fixtures are constructed in-memory by `CreateTestDataStructure()` / `CreateTestDataStructure2()`; no `.dream3d` exemplar or `download_test_data()` entry exists for this filter. | -| Legacy comparison | **Run, and now complete — no configurations outstanding.** Independent three-way A/B (DREAM3D 6.5.171 `PipelineRunner` vs. this branch's `nxrunner` vs. an independent numpy oracle) across six configurations: flat/basic (`AB1`, vs. `MultiThresholdObjects`), nested and inverted-nested (`AB2`/`AB3`), and three deeper shapes added at second-engineer review — three-level nesting, an inverted sibling set, and a grouping-sensitive case (`AB4`–`AB6`, vs. `MultiThresholdObjects2`) — plus a 50M-tuple scale re-run of `AB1`–`AB3`. All sources MATCH in every case. Pre-fix (`develop`): `AB2`/`AB3` diverged (38/100 and 51/100 tuples wrong) — see `-D1`/`-D2`. `AB4`–`AB6` were additionally run through SIMPLNX's SIMPL-pipeline conversion, which surfaced `-D4`. | -| Bug flags | **Three, all resolved, all quantified against real legacy output, all with in-repo regression coverage.** `-D1` — a set combining a leaf threshold with a sibling nested set produced an all-false mask (38/100 tuples wrong) on `develop`. `-D2` — an inverted nested set used `std::reverse` to flip tuple *order* instead of each tuple's value (51/100 wrong) on `develop`. `-D4` — **found at second-engineer review**: converting a legacy pipeline with a nested threshold set flattened the nesting and reset every union operator to AND, so converted `Threshold Objects (Advanced)` pipelines computed a plain AND over all leaves (55/100, 50/100 and 4/100 wrong on `AB4`–`AB6`); pre-existing on `develop`, in shared `ArrayThresholdsParameter` conversion code. See `vv/deviations/MultiThresholdObjectsFilter.md`. | -| V&V phase | Discovery, algorithm relationship, oracle design + reconciliation, algorithm review (fixes applied and re-verified), test inventory, legacy comparison, deviations — **complete**. Oracle chosen and applied (Class 1, corroborated by an independent numpy oracle in the legacy A/B), code paths enumerated (26/27), legacy A/B run and MATCH at both 100-tuple and 50M-tuple scale, 6 deviations documented (`D1`/`D2`/`D4` bugs resolved, each with an in-repo regression test; `D3`/`D5`/`D6` confirmed non-bug capability differences). Second-engineer review of the oracle design and this report **signed off by Michael A. Jackson, 2026-08-19** (PR #1688), which also closed the last three outstanding legacy-comparison items and found `D4`. **Nothing outstanding.** | +| Aspect | Current state | +|--------|---------------| +| Algorithm Relationship | Rewrite. One threshold tree replaces the flat basic filter and the nested Advanced filter in DREAM.3D v6.5.171. | +| Oracle (confirmed) | Class 1 analytical masks are encoded in `test/MultiThresholdObjectsTest.cpp` and `test/MultiThresholdObjectsVvTest.cpp`. All pass in CI. | +| Code paths enumerated | 36 of 51 exercised. Gaps include large chunks, several cancellation points, single-option custom values and basic pipeline conversion. | +| Tests today | 27 test declarations: 23 ordinary cases and 4 template cases. They cover comparisons, nested logic, data types, validation, I/O errors, cancellation and conversion. | +| Exemplar archive | None. Tests construct inputs and expected masks in memory. | +| Legacy comparison | Not run — comparison artifacts are not available. | +| Bug flags | None | +| V&V phase | Analytical verification and second-engineer review are complete. Three fixed defects and three capability differences are documented. | ## Summary -`MultiThresholdObjectsFilter` builds a typed mask array by elementwise-comparing one or more input arrays against user-supplied thresholds, combined through an arbitrarily-nested tree of AND/OR/invert `ArrayThresholdSet`s. Verification uses a **Class 1 (Analytical) oracle**: every comparison operator, invert flag, union operator, nesting depth, custom TRUE/FALSE execution, and both the mask-output and source-input `DataType` are exhaustively hand-derived and asserted in `MultiThresholdObjectsTest.cpp` (14 `TEST_CASE`/`TEMPLATE_TEST_CASE` declarations, all passing). 26 of 27 algorithm, preflight and SIMPL-conversion code paths are exercised. An independent three-way runtime A/B (legacy DREAM3D 6.5.171, this branch, and a numpy oracle) against both legacy predecessors — at 100 tuples and again at 50M tuples — confirms the current implementation matches legacy exactly, and quantifies two real bugs that were present on `develop` and are resolved: `MultiThresholdObjectsFilter-D1` (all-false mask when a set mixes a leaf threshold with a nested set, 38/100 tuples wrong) and `MultiThresholdObjectsFilter-D2` (`std::reverse`-based tuple-order corruption in an inverted nested set instead of per-value inversion, 51/100 tuples wrong). Both now have dedicated in-repo regression coverage (`Valid Threshold Sets` → `ArraySet 6` / `ArraySet 7`), each verified to fail when the algorithm is stubbed out. Second-engineer review extended the A/B to three deeper shapes (`AB4`–`AB6`) and to SIMPLNX's own SIMPL-pipeline conversion path, which surfaced a third bug, `MultiThresholdObjectsFilter-D4`: converting a legacy pipeline containing a nested threshold set flattened the nesting and reset every union operator to AND. Three further deviations (`-D3`, `-D5`, `-D6`) record SIMPLNX-only capabilities with no legacy equivalent — multi-component index selection, a selectable mask output `DataType`, and custom TRUE/FALSE values. +`MultiThresholdObjectsFilter` compares array values with thresholds and combines the results through nested AND, OR and inversion operations. Class 1 analytical fixtures check the mask values, while other tests check validation, cancellation, I/O errors and pipeline conversion. All pass in CI. ## Algorithm Relationship -*Classification:* **Rewrite** +*Classification:* Rewrite -*Evidence:* `SimplnxCoreLegacyUUIDMapping.hpp` maps **two distinct legacy SIMPL UUIDs** to this single SIMPLNX filter's UUID: - -``` -014b7300-cf36-5ede-a751-5faf9b119dae → MultiThresholdObjectsFilter // MultiThresholdObjects ("Threshold Objects") -686d5393-2b02-5c86-b887-dd81a8ae80f2 → MultiThresholdObjectsFilter // MultiThresholdObjects2 ("Threshold Objects (Advanced)") -``` - -`FromSIMPLJson()` correspondingly branches on which legacy UUID (or, for 6.4 pipelines lacking a UUID, which legacy class name) produced the incoming JSON: the basic `MultiThresholdObjects` source is read through `ComparisonSelectionFilterParameterConverter` (flat, AND-only comparison list) and the advanced `MultiThresholdObjects2` source is read through `ComparisonSelectionAdvancedFilterParameterConverter` (nested AND/OR comparison sets). Both are converted into the same `ArrayThresholdSet` argument. This is **not** a line-by-line port of a single legacy algorithm — it's a consolidation of two independently-shipped legacy filters into one, which is why the classification is **Rewrite** rather than Port, per `vv_policy.md`: *"keeping [a UUID relationship] is a claim of functional equivalence... The Deviations file must defend the claim."* Here the claim is stronger than usual — that the merged filter reproduces each of the two legacy filters' behavior when configured equivalently to it. SIMPL 6.4/6.5 conversion fixtures exist at `test/simpl_conversion/6_4/MultiThresholdObjectsFilter.json` and `test/simpl_conversion/6_5/MultiThresholdObjectsFilter.json`, asserting the *argument conversion* round-trips correctly (also now covered by the `SIMPL Backwards Compatibility` `TEST_CASE`). Execution-output equivalence against both legacy filters has now been runtime-A/B-verified on representative flat, nested, and inverted-nested configurations — see Deviations file for the comparison record. - -*Structural differences from each legacy source:* - -1. **Consolidation itself** — one `ArrayThresholdSet` tree replaces two separate legacy parameter models (flat list vs. nested set); the flat legacy model is representable as a one-level `ArrayThresholdSet`. Spot-verified equivalent via runtime A/B (`AB1`, flat config vs. legacy `Threshold Objects`) — see Deviations file. -2. Multi-component index selection added (`#1184`, `32837a30f`) — confirmed **NX-only**: legacy `Threshold Objects (Advanced)`'s `dataCheck()` rejects non-scalar arrays outright (error `-11003`); legacy `Threshold Objects` never had per-component comparison either. Documented as `MultiThresholdObjectsFilter-D3` (non-bug capability addition) in the Deviations file. -3. Custom TRUE/FALSE mask output values added (`#669`, `b65210cf3`) — additive parameter; unconfirmed whether either legacy filter had this option or if it's SIMPLNX-only. Execution-time application (not just preflight bounds-checking) is now covered by the `Valid Execution - Custom Values` `TEMPLATE_TEST_CASE`. -4. Default mask output `DataType` changed to `uint8` (`#1502`, `49919b086`) — unconfirmed against either legacy filter's default. -5. `executeImpl()` body moved into `Algorithms/MultiThresholdObjects.{hpp,cpp}` (`#1544`, `8381d1dd5`) — structural only, no behavior change (internal to SIMPLNX, not a legacy-relationship concern). - -*Material PRs since baseline (2025-10-01):* - -- **#1582** — "ENH: Add missing cancel checks to lots of filters" (`1a42ec6fb`) — cross-cutting PR; added `m_ShouldCancel` checks to many filters including this one (visible today in `MultiThresholdObjects::operator()` and `ComputeThresholdSet`'s per-child cancel check). No output-behavior change on a non-cancelled run. -- **#1605** — "BUG: Fix SIMPL JSON conversion segfault and re-enable backwards-compatibility checks" (`996d7af5a`) — fixed a crash in `FromSIMPLJson()` and re-enabled the SIMPL 6.4/6.5 backwards-compatibility test for this filter (now `SIMPL Backwards Compatibility` in the test file). Affects pipeline-conversion correctness, not execution output. -- This PR itself also rewrote the comparison/combination internals, fixing two bugs — `MultiThresholdObjectsFilter-D1` and `-D2` — see Deviations file. The elementwise comparison itself is unchanged from legacy: `ThresholdFilterHelper::filterDataWithComparision` truncates the comparison value to the input array's type and compares with `std::less`/`std::greater`/`std::equal_to`/`std::not_equal_to` in that type, exactly as `SIMPL/Source/SIMPLib/Filtering/ThresholdFilterHelper.h:60` does (see Oracle → *Comparison-value truncation*). - -*Port-time deltas* (each stated with whether it changes output): - -1. **Combination path consolidated.** The dual apply strategy (direct copy for the first item in a set, AND/OR combine for later items) and the redundant inversion parameter were replaced by a single `ComputeThresholdSet` → `ApplyThresholdValues` → `InsertThreshold` path. **Changes output** — this is the D1/D2 fix, and it restores legacy semantics. -2. **Component-count preflight relaxed.** The check requiring every thresholded array to have the same component count is removed, along with its error code `ErrorCodes::UnequalComponents` (`-4001`) in `MultiThresholdObjectsFilter.hpp`. A threshold set may now mix arrays with different component counts; each threshold selects its own `componentIndex`, and only the *tuple* counts must agree (still enforced via `ErrorCodes::UnequalTuples`). **Does not change output** for any pipeline that previously passed preflight — it widens what is accepted. Pipelines that previously failed with `-4001` now preflight and execute. Removing the enumerator from the public `ErrorCodes` enum is a source-level API break for any out-of-tree plugin referencing `MultiThresholdObjectsFilter::ErrorCodes::UnequalComponents`; nothing in this repository does (verified by grep across `src/` and the plugin tree). -3. **Intermediate storage moved from `std::vector` to `AbstractDataStore`.** One store per nesting level, allocated through `DataStoreUtilities::CreateDataStore`. **Does not change output** — an out-of-core-readiness change only. -4. **SIMPL nested-set conversion no longer flattens.** `ComparisonSelectionAdvancedFilterParameterConverter` now preserves nested `ArrayThresholdSet`s and sets each leaf's union operator from the legacy JSON, instead of collapsing the tree and defaulting every leaf to AND. **Changes output** for converted legacy pipelines — this is the `MultiThresholdObjectsFilter-D4` fix. Added here rather than deferred because the nested-set support this PR adds to the algorithm was unreachable from a converted legacy pipeline without it. +*Evidence:* `src/Plugins/SimplnxCore/src/SimplnxCore/SimplnxCoreLegacyUUIDMapping.hpp` maps both legacy UUIDs to this filter; Direct and Scanline evaluate the same unified threshold-tree model. ## Oracle -*Class:* **1 (Analytical)** - -*Applied:* For a single threshold, `expected[i] = COMPARISON(input[i], value)` (optionally inverted); for a component-indexed array, `input[i]` is replaced by `input[i][componentIndex]`. For a threshold set, `expected` is the boolean combination of each member's own `expected` value: the first member always seeds the accumulator, and the configured `UnionOperator` (AND/OR) combines each subsequent member; the whole set's `expected` is inverted again if the set itself is marked inverted. Every free variable in this formula — comparison operator, invert flag, union operator, set nesting, component index, mask output `DataType`, source-array `DataType`, and custom TRUE/FALSE execution values — is enumerated directly against this closed-form definition in the test file's `Expected*Mask` helper functions and inline hardcoded exemplar arrays, independent of the algorithm's own C++ control flow (`ThresholdFilterHelper`, `InsertThreshold`, `ApplyThresholdValues`). +*Class:* 1 (Analytical). -*Comparison-value truncation (verified, legacy-matching):* the user-supplied comparison value is a `float64`, but `ThresholdFilterHelper::filterDataWithComparision` (`Algorithms/MultiThresholdObjects.cpp`) truncates it to the input array's type and performs the comparison in that type. A threshold of `5.5` against an `int32` array therefore compares against `5`, so `5 == 5.5` is **true** for this filter. This matches legacy DREAM3D exactly — `SIMPL/Source/SIMPLib/Filtering/ThresholdFilterHelper.h:60` performs the identical `static_cast(m_ComparisonValue)` — and it keeps 64-bit integer comparisons exact, which comparing in `float64` would not (values above 2^53 differing by 1 would collapse to equal). The `Expected*Mask` oracle helpers model the same truncation (`static_cast(thresholdValue)` for the int fixtures, `static_cast(...)` on both operands for the `Float32Array` fixture) so the oracle and the implementation agree by construction rather than by coincidence of fixture size. `Valid Single Thresholds: Int` deliberately `GENERATE`s a fractional threshold (`5.5`) against integer data to keep this behavior asserted. +*Applied:* Hand-derived comparisons cast the threshold to the input scalar type, then combine tuple results with AND, OR and logical inversion. -*Encoded:* `test/MultiThresholdObjectsTest.cpp` — +*Encoded:* `src/Plugins/SimplnxCore/test/MultiThresholdObjectsVvTest.cpp::SimplnxCore::MultiThresholdObjects V&V: Valid Threshold Sets` supplies seven tree fixtures; `Exemplar Single Thresholds: Int` and `Exemplar Single Thresholds: Float` supply two literal masks, with comparison and type sweeps in the same file. All pass in CI. -- `Exemplar Single Thresholds: Int` / `: Float` — single fixed-threshold fixtures checked against hardcoded `k_ExemplarInt4` / `k_ExemplarFloat02` arrays -- `Valid Single Thresholds: Int` / `: Float` / `: Int Multi-Component` — comparison operator × invert × (component index for multi-component) sweep, via `ExpectedIntSingleComponentMask` / `ExpectedFloatSingleComponentMask` / `ExpectedIntMultiComponentMask` -- `Valid Threshold Sets` — 7 hand-built configurations: AND / OR / nested-set / nested-set-with-OR / nested-set-with-OR+invert (`CreateThresholdSet1`–`5`, via `ExpectedThresholdSet1Mask`–`5`), plus the two mixed leaf-and-nested-set regression shapes `CreateThresholdSet6`/`7` (via `CheckThresholdSet6`/`7`) -- `Valid Execution, Mask DataType` — 10 mask-output `DataType`s -- `Valid Execution, Input Array DataType` — 11 source-array `DataType`s -- `Valid Execution - Custom Values` — 10 mask `DataType`s, custom TRUE/FALSE values applied at execution time (not just preflight bounds-checked) -- `SIMPL Backwards Compatibility` — SIMPL 6.4/6.5 argument-conversion round-trip -- `SIMPL Nested Set Conversion` — a converted legacy nested threshold set keeps its nesting, inversion flag and union operators -- `Invalid Execution`, `Invalid Execution - Out of Bounds Custom Values` (9 numeric types), `Invalid Execution - Boolean Custom Values` — negative-path fixtures +*Second-engineer review:* Michael Jackson, 2026-08-19. -14 `TEST_CASE`/`TEMPLATE_TEST_CASE` declarations, all pass at HEAD. +## Bugs found and fixed -*Second-engineer review:* **Signed off by Michael A. Jackson , 2026-08-19** (PR #1688 review, per sign-off convention). The V&V work was authored by Matthew Marine, so the review is independent of the author. Reviewed across two passes (2026-07-24 and 2026-08-12) plus this closing pass: +All defects in this table are fixed. DREAM3D-NX v7.4.2 contains the fixes for `MultiThresholdObjectsFilter-D1`, `MultiThresholdObjectsFilter-D2` and `MultiThresholdObjectsFilter-D4`. -- **Oracle design.** Confirmed Class 1 is correct and non-circular — every fixture is built in memory, no `.dream3d` exemplar is consulted, and the `Expected*Mask` helpers re-derive the closed form rather than replaying the algorithm's control flow. Independently corroborated by a three-way A/B (DREAM3D 6.5.171 `PipelineRunner`, this branch's `nxrunner`, and an independent numpy oracle) matching exactly on flat, nested, and inverted-nested configurations at both 100-tuple and 50M-tuple scale — see the Deviations file. -- **Oracle/implementation reconciliation.** The oracle originally compared in `float64` while the implementation truncated the comparison value to the array type. The divergence was masked by the fixture holding only values `0`–`4`; raising the tuple count exposed it. Resolved in favor of the implementation, which matches legacy — the `Expected*Mask` helpers now model the truncation (see *Comparison-value truncation* above). -- **Test load-bearing check.** `Valid Execution, Mask DataType` was found to assert nothing (a hardcoded true/false split had gone stale against a reduced fixture, leaving the true branch unreachable and the expected mask all-false, which the zero-initialized output already satisfied). Verified fixed by stubbing `MultiThresholdObjects::operator()()` to `return {}` — all 10 `SECTION`s now fail. The same stub check was applied to the new `ArraySet 6` / `ArraySet 7` regression fixtures. -- **Code path coverage.** Row 25 was independently confirmed to be a genuine gap — every `CreateThresholdSet*` helper passed either all leaves or all nested sets, never a mix — and is now closed. -- **Closing pass — legacy comparison completed.** The three configurations previously carried as outstanding were closed: custom TRUE/FALSE values and the mask output `DataType` by direct inspection of the legacy sources (neither parameter exists in either legacy filter — recorded as `D6` and `D5`), and the broader configuration sweep by running `AB4`–`AB6` against the legacy `PipelineRunner`. Running those three through SIMPLNX's own SIMPL-pipeline conversion — a path no prior fixture had exercised — surfaced `MultiThresholdObjectsFilter-D4`, fixed here. +| Deviation | Defect | Affected released versions | Resolution | +|-----------|--------|----------------------------|------------| +| `MultiThresholdObjectsFilter-D1` | Nested-set evaluation applied an unused false buffer and overwrote or cleared valid mask values. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | Each child has a separate result; the parent combines that result with the selected operator. | +| `MultiThresholdObjectsFilter-D2` | Inversion could reverse tuple order, omit nested inversion or invert children before combining the root. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | Each node inverts its own completed mask values without changing tuple order. | +| `MultiThresholdObjectsFilter-D4` | Advanced pipeline conversion flattened nested sets and lost leaf union operators. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | Conversion preserves the nested tree, inversion flags and child union operators. | ## Code path coverage -**26 of 27 paths exercised.** Row 13 is a permanent, acceptable gap and the only one remaining. Row 25 was the gap that let `MultiThresholdObjectsFilter-D1` ship and row 27 the gap that let `-D4` ship; both are now covered — see `vv/deviations/MultiThresholdObjectsFilter.md`. - -Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/MultiThresholdObjects.cpp` (~320 lines), plus 7 preflight-only paths in `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/MultiThresholdObjectsFilter.cpp` and the SIMPL-conversion path in `src/simplnx/Parameters/ArrayThresholdsParameter.cpp`. - -Three logical stages: **(a) preflight** validates the threshold set / mask-type / custom-value configuration and stages the output `CreateArrayAction`; **(b) algorithm** recursively evaluates the `ArrayThresholdSet` tree into an internal `bool` mask (per-array comparison combined inline by `ThresholdFilterHelper`, which selects an AND / OR / replace loop once per threshold, then `InsertThreshold`/`ApplyThresholdValues` for cross-node combination) and writes the result into the typed mask array via `ThresholdSetFunctor`, substituting `trueValue`/`falseValue` at that final write; **(c) SIMPL conversion** maps a legacy `MultiThresholdObjects` / `MultiThresholdObjects2` pipeline onto this filter's arguments via `FromSIMPLJson()` and the `ArrayThresholdsParameter` converters. - -| # | Stage | Path | Test case | -|----|-------------------|------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------| -| 1 | (a) Preflight | `thresholdPaths.empty()` → error `-4000` | `Invalid Execution` — "Empty ArrayThresholdSet" | -| 2 | (a) Preflight | Tuple-count mismatch across threshold arrays → `ErrorCodes::UnequalTuples` | `Invalid Execution` — "Mismatching Tuples in Threshold Arrays" | -| 3 | (a) Preflight | `componentIndex >= numComponents` (via `CheckComponentIndicesInThresholds`, recurses into nested sets) → `ErrorCodes::InvalidComponentIndex` | `Invalid Execution` — "Out of Bounds Component Index" | -| 4 | (a) Preflight | `maskArrayType == boolean && useCustomTrueValue` → `CustomTrueWithBoolean` | `Invalid Execution - Boolean Custom Values` — "Custom True Value" | -| 5 | (a) Preflight | `maskArrayType == boolean && useCustomFalseValue` → `CustomFalseWithBoolean` | `Invalid Execution - Boolean Custom Values` — "Custom False Value" | -| 6 | (a) Preflight | `useCustomTrueValue` value outside `[min,max]` of mask type → `CustomTrueOutOfBounds` | `TEMPLATE_TEST_CASE …Out of Bounds Custom Values` — "True Value < Minimum" / "> Maximum", all 9 numeric types | -| 7 | (a) Preflight | `useCustomFalseValue` value outside bounds → `CustomFalseOutOfBounds` | same — "False Value < Minimum" / "> Maximum" | -| 8 | (a) Preflight | Success → stage `CreateArrayAction` for mask output | every "Valid …" test| -| 9 | (b) Algorithm | `ComparisonType::LessThan` | "ArrayThreshold: <" (int / float / multi-component) | -| 10 | (b) Algorithm | `ComparisonType::GreaterThan` | "ArrayThreshold: >" | -| 11 | (b) Algorithm | `ComparisonType::Operator_Equal` | "ArrayThreshold: =="| -| 12 | (b) Algorithm | `ComparisonType::Operator_NotEqual` | "ArrayThreshold: !="| -| 13 | (b) Algorithm | `throw std::runtime_error` for an unrecognized union operator, at the end of `ThresholdFilterHelper::filterDataWithComparision` | *Not directly tested. Unreachable via the public `UnionOperator` enum — both enumerators (`And`, `Or`) are exercised elsewhere.* | -| 14 | (b) Algorithm | `InsertThreshold` with `inverse == true` (flip before combine) | `isInverted = GENERATE(false, true)` in every single-threshold and threshold-set test | -| 15 | (b) Algorithm | `InsertThreshold` with `inverse == false` | same | -| 16 | (b) Algorithm | Combine with `UnionOperator::Or` | `CreateThresholdSet2` (threshold2 = Or), `CreateThresholdSet4`/`5` (nested-set union = Or) | -| 17 | (b) Algorithm | Combine with `UnionOperator::And` | `CreateThresholdSet1` (threshold2/3 = And), `CreateThresholdSet3` default nested And | -| 18 | (b) Algorithm | `ApplyThresholdValues` with `replaceInput == true` (the first item in a set overwrites the accumulator, ignoring its configured union operator) | implicit in every threshold set — first entry of every `CreateThresholdSet*` | -| 19 | (b) Algorithm | `ApplyThresholdValues` with `replaceInput == false` (honors configured operator for later items) | same sets, 2nd/3rd entries | -| 20 | (b) Algorithm | `ComputeThresholdSet` recursion — item is a nested `ArrayThresholdSet` | `CreateThresholdSet3`/`4`/`5` (set-of-sets) | -| 21 | (b) Algorithm | `ComputeThresholdSet` — item is a leaf `ArrayThreshold` | all tests | -| 22 | (b) Algorithm | `ThresholdSetFunctor` dispatch on **mask (output) DataType** | `Valid Execution, Mask DataType` — int8/16/32/64 + uint8/16/32/64 + float32/64 (10 types; no `boolean` `SECTION` in this test — boolean is exercised separately via the default mask type used throughout `RunThresholdSetTest`/other `TEST_CASE`s) | -| 23 | (b) Algorithm | `ExecuteThresholdHelper` dispatch on **source array's DataType** | `Valid Execution, Input Array DataType` — int8/16/32/64 + uint8/16/32/64 + float32/64 + boolean, all 11 types | -| 24 | (b) Algorithm | Multi-component `componentIndex != 0` selection | `Valid Single Thresholds: Int Multi-Component` (`componentIndex = GENERATE(0,1,2)`), plus `componentIndex=1` in Set1, `=0` in Set2 | -| 25 | (b) Algorithm | An `ArrayThresholdSet` whose children mix at least one leaf `ArrayThreshold` with at least one nested `ArrayThresholdSet` (e.g. `{leaf, nestedSet}`, not `{leaf, leaf, leaf}` or `{set, set}`). Historically produced an all-false mask regardless of input (`MultiThresholdObjectsFilter-D1`, confirmed against legacy `Threshold Objects (Advanced)` — 38/100 tuples wrong on `develop`), resolved. | `Valid Threshold Sets` → `ArraySet 6: leaf + nested set` (`CreateThresholdSet6`, a leaf `Int > 2` sibling to a nested set) and `ArraySet 7: leaf + inverted nested set` (`CreateThresholdSet7`, the `AB3` shape), each × `isInverted`. Both were confirmed to fail when `MultiThresholdObjects::operator()()` is stubbed to `return {}` — i.e. they detect the all-false mask that D1 produced. Also confirmed externally by the `AB2`/`AB3` legacy A/B fixtures (see Deviations file), which are not part of the ctest suite. | -| 26 | (b) Algorithm | Custom TRUE/FALSE value substitution at execution time (`ThresholdSetFunctor` writing `trueValue`/`falseValue` from `MultiThresholdObjects::operator()`, not the default `1.0`/`0.0`) — distinct from rows 6–7, which only cover the preflight bounds-check rejecting *out-of-range* custom values and never actually execute with valid ones | `Valid Execution - Custom Values` (`TEMPLATE_TEST_CASE`, 10 numeric mask types), asserting `trueValue`/`falseValue` (25/10 in the test) appear in the output instead of 1/0 | -| 27 | (c) SIMPL conversion | `ComparisonSelectionAdvancedFilterParameterConverter` on a legacy pipeline containing a *nested* `ComparisonSet` — nesting, inversion flag and per-leaf union operators must all survive conversion (`MultiThresholdObjectsFilter-D4`) | `SIMPL Nested Set Conversion`, plus the `AB4`–`AB6` converted-pipeline runs recorded in the Deviations file | - -Not counted as an algorithm/preflight path: the "Empty ArrayThreshold DataPath" section of `Invalid Execution` exercises `ArrayThresholdsParameter`'s own path-existence validation, which runs before `preflightImpl` is called — it's a parameter-layer gate, not code inside this filter or algorithm. - -`MultiThresholdObjectsFilter-D2` (the pre-fix `std::reverse` tuple-order bug) does not get its own row: the buggy code path no longer exists on this branch, and its trigger — a leaf combined with an *inverted* nested set (the legacy A/B's `AB3` shape) — overlaps with row 25's mixed-sibling shape rather than isolating D2 cleanly on its own. It is covered in-repo by the `ArraySet 7` `SECTION` listed against row 25. +36 of 51 paths exercised. Each uncovered row states the missing fixture or injection point. + +Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/MultiThresholdObjects.cpp` (38 lines); `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/MultiThresholdObjectsDirect.cpp` (240 lines); `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/MultiThresholdObjectsScanline.cpp` (324 lines); `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/MultiThresholdObjectsFilter.cpp` (293 lines); `src/simplnx/Parameters/ArrayThresholdsParameter.cpp` (306 lines). + +Test names below omit their common namespace prefix. `V&V:` identifies cases in `MultiThresholdObjectsVvTest.cpp`; other names refer to `MultiThresholdObjectsTest.cpp`. The scenario harness selects Direct and Scanline on arrays in memory. + +| # | Phase | Path | Test case | +|---|-------|------|-----------| +| 1 | Preflight | No required input arrays → error `-4000` | V&V: `Invalid Execution` / `Empty ArrayThresholdSet` | +| 2 | Preflight | Input tuple counts differ → `UnequalTuples` | V&V: `Invalid Execution` / `Mismatching Tuples in Threshold Arrays` | +| 3 | Preflight | Component index exceeds the array bounds → `InvalidComponentIndex` | V&V: `Invalid Execution` / `Out of Bounds Component Index` | +| 4 | Preflight | Boolean output with custom TRUE enabled → `CustomTrueWithBoolean` | V&V: `Invalid Execution - Boolean Custom Values` / `Custom True Value` | +| 5 | Preflight | Boolean output with custom FALSE enabled → `CustomFalseWithBoolean` | V&V: `Invalid Execution - Boolean Custom Values` / `Custom False Value` | +| 6 | Preflight | Custom TRUE is outside the output type range → `CustomTrueOutOfBounds` | V&V: `Invalid Execution - Out of Bounds Custom Values` — below minimum and above maximum | +| 7 | Preflight | Custom FALSE is outside the output type range → `CustomFalseOutOfBounds` | V&V: `Invalid Execution - Out of Bounds Custom Values` — below minimum and above maximum | +| 8 | Preflight | Valid inputs → create a scalar mask with the input tuple shape | `Valid Execution` — checks the output action, type, store and mask values | +| 9 | Parameter validation | Threshold input path is absent → reject the argument | V&V: `Invalid Execution` / `Empty ArrayThreshold DataPath` | +| 10 | Dispatch | Select Direct for arrays in memory | `Valid Execution`; V&V: `Valid Threshold Sets` — algorithm scenario harness | +| 11 | Dispatch | Select Scanline for arrays in memory | `Valid Execution`; V&V: `Valid Threshold Sets` — algorithm scenario harness | +| 12 | Comparison | Less-than comparison in Direct and Scanline | V&V: `Valid Single Thresholds: Int`, `Float`, `Int Multi-Component` / `ArrayThreshold: <` | +| 13 | Comparison | Greater-than comparison in Direct and Scanline | V&V: `Valid Single Thresholds: Int`, `Float`, `Int Multi-Component` / `ArrayThreshold: >` | +| 14 | Comparison | Equal comparison in Direct and Scanline | V&V: `Valid Single Thresholds: Int`, `Float`, `Int Multi-Component` / `ArrayThreshold: ==` | +| 15 | Comparison | Not-equal comparison in Direct and Scanline | V&V: `Valid Single Thresholds: Int`, `Float`, `Int Multi-Component` / `ArrayThreshold: !=` | +| 16 | Comparison | Unknown comparison value → throw | *Not directly tested. No fixture supplies an invalid comparison enumerator.* | +| 17 | Comparison | Dispatch all 11 input scalar types | V&V: `Valid Execution, Input Array DataType` — ten numeric types and boolean | +| 18 | Output | Dispatch all 11 mask scalar types | V&V: `Valid Execution, Mask DataType` — ten numeric types; `Valid Execution` — boolean | +| 19 | Comparison | Read a selected nonzero component | V&V: `Valid Single Thresholds: Int Multi-Component` — indices 0, 1 and 2 | +| 20 | Tree | First child seeds the result; later children combine with it | V&V: `Valid Threshold Sets` — `ArraySet 1` through `ArraySet 7` | +| 21 | Tree | Combine child results with AND | V&V: `Valid Threshold Sets` / `ArraySet 1`, `ArraySet 3` | +| 22 | Tree | Combine child results with OR | V&V: `Valid Threshold Sets` / `ArraySet 2`, `ArraySet 4` | +| 23 | Tree | Invert or retain each leaf result | V&V: `Valid Single Thresholds: Int`, `Float`, `Int Multi-Component` — both inversion states | +| 24 | Tree | Invert a nested set after evaluating its children | V&V: `Valid Threshold Sets` / `ArraySet 5`, `ArraySet 7: leaf + inverted nested set` | +| 25 | Tree | Invert the completed root result | `Top-level inversion is logical`; `Nested sets compose before root inversion` | +| 26 | Tree | Evaluate mixed leaf and nested-set siblings | V&V: `Valid Threshold Sets` / `ArraySet 6: leaf + nested set`, `ArraySet 7: leaf + inverted nested set` | +| 27 | Output | Use default 1/0 values or enable both custom values | `Valid Execution`; V&V: `Valid Execution - Custom Values` — 25/10 for ten numeric types | +| 28 | Output | Enable only custom TRUE during execution | *Not directly tested. Successful custom-value fixtures enable both options.* | +| 29 | Output | Enable only custom FALSE during execution | *Not directly tested. Successful custom-value fixtures enable both options.* | +| 30 | Direct cancellation | Cancel before evaluation; preserve output | `Direct and Scanline honor cancellation without completing output` / `Direct` | +| 31 | Direct cancellation | Cancel during leaf evaluation, recursion, merge, inversion or output writes | *Not directly tested. The Direct cancellation fixture sets the flag before entry.* | +| 32 | Scanline I/O | Read and write a short chunk | `Direct and Scanline evaluate the same nested tree` — 20 tuples | +| 33 | Scanline I/O | Process a full 65,536-tuple chunk and a later tail | *Not directly tested. Current fixtures have at most 20 tuples.* | +| 34 | Scanline I/O | Propagate an input bulk-read error through recursive evaluation | `Scanline propagates bulk I/O failures` / `input bulk read` — error `-73001` | +| 35 | Scanline I/O | Propagate an output bulk-write error | `Scanline propagates bulk I/O failures` / `output bulk write` — error `-73002` | +| 36 | Scanline cancellation | Cancel after reading input; return before writing output | `Direct and Scanline honor cancellation without completing output` / `Scanline` | +| 37 | Scanline cancellation | Cancel at node entry or before the first chunk | *Not directly tested. The Scanline fixture cancels after its first read.* | +| 38 | Scanline cancellation | Cancel during child iteration, merge, inversion or between completed chunks | *Not directly tested. The cancellation fixture has one leaf and one short chunk.* | +| 39 | Tree | Empty nested set yields false, or true when inverted | *Not directly tested. The empty root fixture fails preflight; no valid tree contains an empty child set.* | +| 40 | Output | Zero input tuples → no output writes | *Not directly tested. All valid fixtures contain tuples.* | +| 41 | Tree | Node is neither a leaf nor a set | *Not directly tested. Fixtures use only the two supported threshold classes.* | +| 42 | Scanline merge | The internal `insertThresholdChunk` inversion argument is true | *Not directly tested. Its only caller passes false; node inversion occurs separately.* | +| 43 | SIMPL conversion | Select Advanced conversion by UUID | `SIMPL Backwards Compatibility` / `SIMPL 6.5 (UUID)` | +| 44 | SIMPL conversion | Select Advanced conversion by class name | `SIMPL Backwards Compatibility` / `SIMPL 6.4 (Filter_Name)` | +| 45 | SIMPL conversion | Select basic flat-list conversion | *Not directly tested. Both compatibility fixtures identify the Advanced filter.* | +| 46 | SIMPL conversion | Preserve nested sets, child union operators and inversion | V&V: `SIMPL Nested Set Conversion` | +| 47 | SIMPL conversion | Convert a supplied `ScalarType` | `SIMPL Backwards Compatibility` — fixtures supply the key; assertions check identity and output name | +| 48 | SIMPL conversion | Use default mask type when `ScalarType` is absent | V&V: `SIMPL Nested Set Conversion` — fixture omits the key; assertions check the threshold tree | +| 49 | SIMPL conversion | Reject a basic parameter that is not an array | *Not directly tested. No malformed basic conversion fixture exists.* | +| 50 | SIMPL conversion | Reject an Advanced parameter that is not an object or whose thresholds are not an array | *Not directly tested. No malformed Advanced conversion fixture exists.* | +| 51 | Dispatch | Select Scanline when an input or the output uses an external store | *Not directly tested. These test files construct stores in memory.* | ## Test inventory +The two files contain 27 test declarations. All pass in CI. Template type counts and dynamic sections are stated in Notes. + | Test case | Status | Notes | |-----------|--------|-------| -| `Exemplar Single Thresholds: Int` | kept | Single fixed threshold (`Int32 > 4`) checked against hardcoded `k_ExemplarInt4` array. | -| `Exemplar Single Thresholds: Float` | kept | Single fixed threshold (`Float32 == 0.02`) checked against hardcoded `k_ExemplarFloat02` array. | -| `Valid Single Thresholds: Int` | kept | `GENERATE` over 8 threshold values × 2 invert states, 4 `SECTION`s (`>`, `<`, `==`, `!=`) against `k_TestArrayIntPath`; every tuple checked via `ExpectedIntSingleComponentMask`. Modified this cycle: `ExpectedIntSingleComponentMask` now truncates the threshold to `int32` so the oracle matches the implementation's (and legacy's) comparison-value cast — the `5.5` case is what exposed the mismatch. The complementary-operator re-assertions in the `==` / `!=` `SECTION`s were dropped; they restated the oracle rather than testing the filter. | -| `Valid Single Thresholds: Float` | kept | Same sweep against `k_TestArrayFloatPath` via `ExpectedFloatSingleComponentMask`. Modified this cycle: the oracle rounds both operands through `float32` to match the `Float32Array` fixture, and the complementary-operator re-assertions were dropped for the same reason as the `Int` case. | -| `Valid Single Thresholds: Int Multi-Component` | kept | Adds `componentIndex = GENERATE(0,1,2)` against `k_MultiComponentArrayPath`. Modified this cycle with the same oracle-truncation and re-assertion changes as the single-component `Int` case. | -| `Valid Threshold Sets` | kept | 7 `SECTION`s, each × `isInverted`. `ArraySet 1`–`5` cover AND, OR, nested-set, nested-set-with-OR, and nested-set-with-OR+invert combinations. `ArraySet 6` / `ArraySet 7` were **added new-for-V&V**: a leaf threshold with a sibling nested set, and the same with the nested set inverted — the `MultiThresholdObjectsFilter-D1` / `-D2` trigger shapes (code-path row 25). Both were confirmed load-bearing by stubbing the algorithm to `return {}`. | -| `Invalid Execution` | kept | 4 `SECTION`s: empty threshold set (`-4000`), empty threshold `DataPath` (parameter-layer validation), out-of-bounds component index (`InvalidComponentIndex`), mismatched tuple counts (`UnequalTuples`). | -| `Invalid Execution - Out of Bounds Custom Values` (`TEMPLATE_TEST_CASE`) | kept | 9 numeric-type instantiations × 4 `SECTION`s (true/false value below minimum / above maximum) — `CustomTrueOutOfBounds` / `CustomFalseOutOfBounds`. | -| `Invalid Execution - Boolean Custom Values` | kept | 2 `SECTION`s — custom TRUE/FALSE value rejected when mask type is `boolean`. | -| `Valid Execution, Mask DataType` | kept | 10 `SECTION`s, one per mask-output `DataType` (int8…float64 — no `boolean` `SECTION`; boolean covered via the default mask type used throughout the other `TEST_CASE`s). Modified this cycle: the expected true/false split had been left as a hardcoded literal against a reduced fixture, which made the true branch unreachable and the expected mask all-false — the test asserted nothing. The split is now derived from the fixture constants with a `static_assert` that it lies inside the tuple range. | -| `Valid Execution, Input Array DataType` | kept | 11 `SECTION`s, one per **source-array** `DataType` (int8…float64, bool). | -| `SIMPL Nested Set Conversion` | new-for-V&V | Converts a legacy `MultiThresholdObjects2` pipeline whose thresholds are a leaf sibling to an *inverted* nested set with OR-joined children, and asserts the converted `ArrayThresholdSet` keeps the nesting, the inversion flag, the swapped comparison operators, and the children's union operators. Added for `MultiThresholdObjectsFilter-D4`; verified load-bearing (fails against the pre-fix converter). Fixture: `test/simpl_conversion/6_5/MultiThresholdObjectsFilter_Nested.json`. | -| `SIMPL Backwards Compatibility` | kept | `DYNAMIC_SECTION` over the SIMPL 6.4 and 6.5 conversion fixtures; asserts pipeline conversion round-trips (UUID + one argument value). Re-enabled by `#1605` after a prior segfault. Deleted earlier in this PR and restored during V&V review, so the `#1605` coverage is not lost. | -| `Valid Execution - Custom Values` (`TEMPLATE_TEST_CASE`) | kept | 10 numeric-type instantiations; asserts custom TRUE (`25`) / FALSE (`10`) values are actually written to the output mask at execution time — closes the code-path gap on row 26. Deleted earlier in this PR and restored during V&V review; `UnitTest::CheckArraysInheritTupleDims` was added at the same time. | - -**Missing:** nothing outstanding. The last gap — a set mixing a leaf threshold with a sibling nested set (row 25), the shape that triggered `MultiThresholdObjectsFilter-D1` — is closed by the `ArraySet 6` / `ArraySet 7` `SECTION`s of `Valid Threshold Sets`. - -**Count basis:** 31 ctest entries = 12 single-entry `TEST_CASE`s (1 ctest entry each) + `Invalid Execution - Out of Bounds Custom Values` (`TEMPLATE_TEST_CASE`, 9 types → 9 entries) + `Valid Execution - Custom Values` (`TEMPLATE_TEST_CASE`, 10 types → 10 entries), verified by directly reading each declaration's type list twice. `catch_discover_tests` is called with no filtering options in `cmake/Plugin.cmake:404`, so Catch2's default behavior applies: one ctest entry per `TEMPLATE_TEST_CASE` type instantiation. A count of 28 entries cited during review was correct at the commit it was measured against; the two `Exemplar Single Thresholds` `TEST_CASE`s added afterwards account for the difference. 31 matches the test file as it stands today. - -**Dual-build verification at sign-off:** `ctest -R MultiThresholdObjects` passes 31/31 in **both** builds — in-core (`NX-Com-Qt69-Vtk96-Rel`) and out-of-core (`NX-OOC-Qt69-Vtk95-Rel`). The full `SimplnxCore::` suite also passes 984/984 in-core. The OOC run initially reported all entries failing; all were `LoadPlugins()` aborting because stale plugin binaries in that build directory linked an older TBB soname, and all cleared after rebuilding those plugins — no failure was attributable to this filter. +| `SimplnxCore::MultiThresholdObjects: Valid Execution` | kept | Checks 20-tuple float and integer masks, output allocation and in-memory storage. | +| `SimplnxCore::MultiThresholdObjects: Top-level inversion is logical` | kept | Checks `NOT(i > 10)` for all 20 tuples with boolean output. | +| `SimplnxCore::MultiThresholdObjects: Nested sets compose before root inversion` | kept | Checks `NOT(i > 2 AND (i > 10 OR i < 5))` for all 20 tuples. | +| `SimplnxCore::MultiThresholdObjects: Scanline propagates bulk I/O failures` | kept | Injects bulk-read and bulk-write failures and checks errors `-73001` and `-73002`. | +| `SimplnxCore::MultiThresholdObjects: Direct and Scanline evaluate the same nested tree` | kept | Compares Direct and Scanline outputs for an inverted nested tree with component selection and custom values 19/4. This is a consistency check. | +| `SimplnxCore::MultiThresholdObjects: Direct and Scanline honor cancellation without completing output` | kept | Checks unchanged output after cancellation before Direct entry and after the first Scanline read. | +| `SimplnxCore::MultiThresholdObjects: Valid Execution - Custom Values` | kept | Template case over ten numeric output types; checks TRUE=25 and FALSE=10 for `Int32 > 15`. | +| `SimplnxCore::MultiThresholdObjects: Invalid Execution` | kept | Checks empty thresholds, a missing path, an invalid component index and unequal tuple counts. | +| `SimplnxCore::MultiThresholdObjects: Invalid Execution - Out of Bounds Custom Values` | kept | Template case over nine numeric types; checks custom TRUE and FALSE below the minimum and above the maximum. | +| `SimplnxCore::MultiThresholdObjects: Invalid Execution - Boolean Custom Values` | kept | Checks rejection of custom TRUE or FALSE with boolean output. | +| `SimplnxCore::MultiThresholdObjects: Valid Execution, DataType` | kept | Checks threshold output values for ten numeric mask types. | +| `SimplnxCore::MultiThresholdObjects: Valid Execution - Multicomponent` | kept | Checks component 1 of a three-component array, including the zero tuple and alternating signs. | +| `SimplnxCore::MultiThresholdObjectsFilter: SIMPL Backwards Compatibility` | kept | Dynamic sections `SIMPL 6.5 (UUID)` and `SIMPL 6.4 (Filter_Name)` check converted filter identity and output name; both fixtures use the Advanced filter. | +| `SimplnxCore::MultiThresholdObjects V&V: Exemplar Single Thresholds: Int` | kept | Checks the literal eight-value mask `[0,0,0,0,0,1,1,1]` for `Int32 > 4`. | +| `SimplnxCore::MultiThresholdObjects V&V: Exemplar Single Thresholds: Float` | kept | Checks the literal eight-value mask `[0,1,0,0,0,0,0,0]` for `Float32 == 0.02`. | +| `SimplnxCore::MultiThresholdObjects V&V: Valid Single Thresholds: Int` | kept | Sweeps four operators, eight thresholds and both inversion states. The oracle casts the threshold to int32, including 5.5. | +| `SimplnxCore::MultiThresholdObjects V&V: Valid Single Thresholds: Float` | kept | Sweeps four operators, six thresholds and both inversion states. The oracle rounds both operands to float32. | +| `SimplnxCore::MultiThresholdObjects V&V: Valid Single Thresholds: Int Multi-Component` | kept | Sweeps four operators, eight thresholds, both inversion states and component indices 0, 1 and 2. | +| `SimplnxCore::MultiThresholdObjects V&V: Valid Threshold Sets` | kept | Seven tree fixtures check AND, OR, nested sets and inversion. ArraySet 6 and ArraySet 7 add regressions for D1 and D2. | +| `SimplnxCore::MultiThresholdObjects V&V: Invalid Execution` | kept | Checks empty thresholds, a missing path, an invalid component index and unequal tuple counts. | +| `SimplnxCore::MultiThresholdObjects V&V: Invalid Execution - Out of Bounds Custom Values` | kept | Template case over nine numeric types; checks all four custom-value range errors. | +| `SimplnxCore::MultiThresholdObjects V&V: Invalid Execution - Boolean Custom Values` | kept | Checks rejection of custom TRUE or FALSE with boolean output. | +| `SimplnxCore::MultiThresholdObjects V&V: Valid Execution, Mask DataType` | kept | Checks ten numeric output types. The fixture split selects both false and true values, so an unchanged zero mask fails. | +| `SimplnxCore::MultiThresholdObjects V&V: Valid Execution, Input Array DataType` | kept | Checks all eleven input types against an int8 mask, including a separate boolean expectation. | +| `SimplnxCore::MultiThresholdObjectsFilter V&V: SIMPL Nested Set Conversion` | new-for-V&V | Checks converted leaf comparisons, nested grouping, inversion and OR preservation for D4. It checks arguments, not execution output. | +| `SimplnxCore::MultiThresholdObjectsFilter V&V: SIMPL Backwards Compatibility` | kept | Dynamic sections `SIMPL 6.5 (UUID)` and `SIMPL 6.4 (Filter_Name)` check converted identity and output name for the Advanced filter. | +| `SimplnxCore::MultiThresholdObjects V&V: Valid Execution - Custom Values` | kept | Template case over ten numeric mask types; checks TRUE=25 and FALSE=10 for `Int32 > 3`, plus inherited tuple dimensions. | ## Exemplar archive -None. All fixtures for this filter are constructed in-memory in `test/MultiThresholdObjectsTest.cpp` (`CreateTestDataStructure()`, `CreateTestDataStructure2()`); there is no `.dream3d` exemplar and no `download_test_data()` entry in `test/CMakeLists.txt` for `MultiThresholdObjectsFilter`. No provenance sidecar is needed. - -## Deviations from DREAM3D 6.5.171 - -Legacy comparison **run and complete**: independent three-way A/B (DREAM3D 6.5.171 `PipelineRunner`, this branch's `nxrunner`, and a numpy oracle) across six configurations — flat, nested, inverted-nested, three-level nested, inverted sibling set, and a grouping-sensitive case — at 100 tuples, with the first three re-run at 50M tuples. All sources MATCH in every case. No configurations remain outstanding. Full record in `vv/deviations/MultiThresholdObjectsFilter.md`. +- **Archive:** None. Both test files construct their array fixtures in memory; `src/Plugins/SimplnxCore/test/CMakeLists.txt` registers no archive for this filter. +- **SHA512:** Not applicable; no archive is registered. +- **Provenance:** Not applicable; expected masks are defined in the test source. -- `MultiThresholdObjectsFilter-D1` — a set mixing a leaf threshold with a sibling nested set produced an all-false mask on `develop` (38/100 tuples wrong vs. legacy `Threshold Objects (Advanced)`). **Fixed by this PR**, with in-repo regression coverage (`Valid Threshold Sets` → `ArraySet 6`). -- `MultiThresholdObjectsFilter-D2` — a leaf combined with an inverted nested set used `std::reverse` to flip tuple order instead of flipping each tuple's value on `develop` (51/100 tuples wrong vs. the same legacy filter). **Fixed by this PR**, with in-repo regression coverage (`Valid Threshold Sets` → `ArraySet 7`). -- `MultiThresholdObjectsFilter-D3` — multi-component index selection is SIMPLNX-only; legacy `Threshold Objects (Advanced)` rejects non-scalar arrays (`dataCheck()` error `-11003`). Not a bug — a deliberate SIMPLNX capability addition, documented for migration guidance. -- `MultiThresholdObjectsFilter-D4` — converting a legacy pipeline containing a nested threshold set flattened the nesting and reset every union operator to AND, so a converted `Threshold Objects (Advanced)` pipeline computed a plain AND over all leaves (55/100, 50/100 and 4/100 tuples wrong on `AB4`–`AB6`). Pre-existing on `develop`, in `src/simplnx/Parameters/ArrayThresholdsParameter.cpp`. **Fixed by this PR**, with in-repo regression coverage (`SIMPL Nested Set Conversion`). -- `MultiThresholdObjectsFilter-D5` — the mask output `DataType` is selectable in SIMPLNX and defaults to `uint8`; both legacy filters always created a `DataArray`. Values are identical, the array type is not. Not a bug — set **Mask Type** to `boolean` when bit-for-bit legacy parity of the output file matters. -- `MultiThresholdObjectsFilter-D6` — custom TRUE/FALSE mask values are SIMPLNX-only; neither legacy filter has the parameter. Not a bug; the defaults (1/0) reproduce legacy behavior. +## Deviations from DREAM.3D v6.5.171 -**Fixed in DREAM3D-NX 7.4.2:** `MultiThresholdObjectsFilter-D1`-`D2`, `MultiThresholdObjectsFilter-D4`. +See `vv/deviations/MultiThresholdObjectsFilter.md` for the root cause, affected users and recommendation. This table lists Part 1 only. +| Deviation | Observed difference | +|-----------|---------------------| +| `MultiThresholdObjectsFilter-D3` | SIMPLNX accepts a selected component of a multi-component array; both legacy filters require scalar arrays. | +| `MultiThresholdObjectsFilter-D5` | SIMPLNX offers selectable mask types and defaults to uint8; both legacy filters create boolean masks. | +| `MultiThresholdObjectsFilter-D6` | SIMPLNX offers custom TRUE and FALSE values; both legacy filters write boolean values. | diff --git a/src/Plugins/SimplnxCore/vv/deviations/MultiThresholdObjectsFilter.md b/src/Plugins/SimplnxCore/vv/deviations/MultiThresholdObjectsFilter.md index 2c6019f238..c784a574d1 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/MultiThresholdObjectsFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/MultiThresholdObjectsFilter.md @@ -1,190 +1,133 @@ -# Deviations from DREAM3D 6.5.171: MultiThresholdObjectsFilter +# Deviations from DREAM.3D v6.5.171: MultiThresholdObjectsFilter -This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent. - -Entries are referenced by stable ID (`MultiThresholdObjectsFilter-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. - -## Filter UUID - -`4246245e-1011-4add-8436-0af6bed19228` - -## Headline - -**6 deviations documented: 3 bugs (all in SIMPLNX, all resolved, all with in-repo regression tests), 3 confirmed non-bug capability differences.** Legacy comparison has been **run**: an independent three-way A/B — DREAM3D 6.5.171 `PipelineRunner`, this branch's `nxrunner`, and an independent numpy oracle — on a shared 100-tuple fixture, covering representative flat (`Threshold Objects`), nested, and inverted-nested (`Threshold Objects (Advanced)`) configurations, re-run again at 50M tuples. Post-fix, all three sources MATCH in every case at both scales. The in-repo `MultiThresholdObjectsTest.cpp` suite (14 `TEST_CASE`/`TEMPLATE_TEST_CASE` declarations / 31 ctest entries — see the V&V report's Test inventory for the count basis) also passes locally. - -The same three pipelines run against `develop` (pre-fix) reproduce two real bugs quantitatively: `MultiThresholdObjectsFilter-D1` (38/100 tuples wrong) and `MultiThresholdObjectsFilter-D2` (51/100 tuples wrong). Both are **resolved** — not by a commit that predates this V&V pass. Both now have dedicated in-repo regression coverage as well: the `ArraySet 6` and `ArraySet 7` `SECTION`s of `Valid Threshold Sets` (see the V&V report's Code path coverage row 25 and Test inventory), each confirmed to fail when the algorithm is stubbed out. - -`MultiThresholdObjectsFilter-D3`, `-D5`, and `-D6` document confirmed, deliberate capability differences (not bugs): multi-component index selection, custom TRUE/FALSE mask values, and a selectable mask output `DataType` all exist only in SIMPLNX. - -`MultiThresholdObjectsFilter-D4` is a third SIMPLNX bug, found during second-engineer review and resolved: converting a legacy *nested* threshold set flattened it and discarded every union operator, so converted `Threshold Objects (Advanced)` pipelines computed a plain AND over all leaves. +Part 1 lists three capability differences from DREAM.3D v6.5.171. Part 2 lists three fixed SIMPLNX defects. All entries keep their existing IDs. --- -## Comparison method - -| | | -|---|---| -| **Comparison type** | Runtime three-way A/B: legacy DREAM3D 6.5.171 (`PipelineRunner`) vs. this branch (`nxrunner`) vs. an independent numpy oracle | -| **Shared input** | Legacy-format fixture, 100 tuples: `Int32 = 0..99`, `Float32 = 0.01*(i+1)` | -| **Scale re-run** | Same three configurations (AB1–AB3) re-run at 50M random tuples — this branch matches the numpy oracle exactly at scale | -| **Deeper-nesting fixture** | Second shared 100-tuple fixture generated by the legacy runner itself (`CreateDataArray`, random-with-range: `Int32` in [0,100), `Float32` in [0,1)), read by both runners from that same file. Used for `AB4`–`AB6` | -| **In-repo regression suite** | `test/MultiThresholdObjectsTest.cpp` passes locally at the verified commit (14 `TEST_CASE`/`TEMPLATE_TEST_CASE` declarations / 31 ctest entries; see the V&V report's Test inventory for the count basis) | - -### Per-configuration result (100-tuple fixture, this branch = post-fix) +## Part 1 — Differences from DREAM.3D v6.5.171 -| Case | Config | Legacy filter | This branch vs. legacy vs. oracle | -|---|---|---|---| -| `AB1` | `Int32 > 42 AND Float32 < 0.70` (flat) | `MultiThresholdObjects` ("Threshold Objects") | **MATCH** (legacy = NX = oracle) | -| `AB2` | `Int32 > 20 AND (Float32 < 0.60 OR Int32 == 55)` (nested set) | `MultiThresholdObjects2` ("Threshold Objects (Advanced)") | **MATCH** | -| `AB3` | `Int32 < 80 OR NOT(Int32 > 30 AND Float32 < 0.95)` (inverted nested set) | `MultiThresholdObjects2` | **MATCH** | - -### Deeper-nesting configurations (added at second-engineer review, 2026-08-19) - -These close the "broader configuration sweep" item that was previously outstanding. Each was run three ways — legacy `MultiThresholdObjects2`, a natively-authored SIMPLNX pipeline, and an independent numpy oracle written from the English description — and a fourth way, through SIMPLNX's own conversion of the legacy SIMPL pipeline JSON. - -| Case | Config | Shape | Legacy vs. oracle | SIMPLNX native vs. oracle | SIMPLNX converted-from-SIMPL vs. oracle | -|---|---|---|---|---|---| -| `AB4` | `Int32 > 20 AND (Float32 < 0.8 OR (Int32 < 60 AND Float32 > 0.3))` | 3 levels deep | **MATCH** 100/100 | **MATCH** 100/100 | **MATCH** 100/100 (45/100 before the `D4` fix) | -| `AB5` | `(Int32 > 30 OR Float32 < 0.2) AND NOT(Int32 < 70 AND Float32 > 0.4)` | two sibling sets, second inverted | **MATCH** 100/100 | **MATCH** 100/100 | **MATCH** 100/100 (50/100 before) | -| `AB6` | `Int32 < 50 AND (Float32 > 0.9 OR Int32 == 3)` | grouping-sensitive, only 4 true tuples | **MATCH** 100/100 | **MATCH** 100/100 | **MATCH** 100/100 (96/100 before — an all-false mask) | - -`AB6` is deliberately built so the correct answer (4 true tuples) and the answer produced by discarding the AND/OR grouping (0 true tuples) differ, so it fails loudly if grouping is not honored. - -### Pre-fix (`develop`) result, same three pipelines - -| Case | Result on `develop` (pre-fix) | Deviation | -|---|---|---| -| `AB1` | Matches legacy/oracle (the flat config was never affected — see D1/D2 root causes) | none | -| `AB2` | **All-false mask — 38/100 tuples wrong** | `MultiThresholdObjectsFilter-D1` | -| `AB3` | **Differs in 51/100 values** | `MultiThresholdObjectsFilter-D2` | - -Both bug-fix claims in the PR are real, and the fix restores legacy semantics: legacy's `invertThreshold()` flips values element-wise; the old SIMPLNX `std::reverse` was a misport of that, and the old per-item functor dispatch broke nested-set combination entirely. (Engineer's account.) - ---- - -## MultiThresholdObjectsFilter-D1 +### MultiThresholdObjectsFilter-D3 | Field | Value | |---|---| -| **Deviation ID** | `MultiThresholdObjectsFilter-D1` | +| **Deviation ID** | `MultiThresholdObjectsFilter-D3` | | **Filter UUID** | `4246245e-1011-4add-8436-0af6bed19228` | -| **Status** | active in all releases through **7.4.1**; resolved in PR #1688 (2026-08-20) — resolved in DREAM3D-NX **7.4.2** | +| **Status** | active | -**Symptom:** On `develop`, an `ArrayThresholdSet` whose children mix at least one leaf `ArrayThreshold` with at least one nested `ArrayThresholdSet` (e.g. `AB2`: `{leaf: Int32 > 20, nestedSet: (Float32 < 0.60 OR Int32 == 55)}`) produced an all-false mask, regardless of input data. Quantified on the `AB2` fixture: **38 of 100 tuples wrong** (all forced false) vs. legacy `Threshold Objects (Advanced)` and the numpy oracle. +**Symptom:** SIMPLNX can threshold one selected component of a multi-component array. Both DREAM.3D v6.5.171 filters require scalar arrays. -**Root cause:** Bug (SIMPLNX-side, `develop`). Per the reporting engineer: the old per-item functor dispatch broke nested-set combination entirely — a redundant inversion/apply mechanism was threaded separately from each node's own `isInverted()`, combined with a dual apply strategy (direct-copy for the first item in a set vs. an AND/OR combine for later items against an accumulator pre-filled false) that this PR replaced with a single, consistent combination path. The exact instruction-level trace of why this specifically zeroed the mask for the mixed-leaf/nested-set shape on `develop` was not independently re-derived line-by-line for this report; the symptom and fix are established by the `AB2` runtime comparison against real legacy output, which is stronger evidence than a re-derived trace would be. +**Root cause:** algorithmic choice. Each SIMPLNX leaf stores its own component index. Preflight checks that index against its input array. Both legacy filters reject non-scalar arrays with error `-11003`. -**Affected users:** Any pipeline (SIMPLNX-native, or converted from legacy `Threshold Objects (Advanced)`) using a threshold set that combines a plain leaf comparison with a sibling nested group — a common shape, not an exotic edge case. Silent on `develop`: the filter reported success and wrote a fully-false mask with no warning. +**Affected users:** Users who threshold vector or other multi-component arrays. This capability has no equivalent in the legacy filters. -**Regression coverage:** `Valid Threshold Sets` → `ArraySet 6: leaf + nested set` (`CreateThresholdSet6`/`CheckThresholdSet6` in `test/MultiThresholdObjectsTest.cpp`), swept over `isInverted`. Verified load-bearing: stubbing `MultiThresholdObjects::operator()()` to `return {}` — which reproduces D1's all-false mask, since the output array is zero-initialized at creation — makes this `SECTION` fail. +**Recommendation:** trust SIMPLNX. Select the required component explicitly; a legacy pipeline must first extract that component into a scalar array. -**Recommendation:** Trust SIMPLNX (confirmed bit-for-bit against legacy on `AB2` at both 100 tuples and 50M tuples). The pre-fix output was unconditionally wrong; anyone on a released version through 7.4.1 should upgrade and re-verify any pipeline outputs generated before the fix. +**Evidence:** `src/Plugins/SimplnxCore/test/MultiThresholdObjectsVvTest.cpp`, `Valid Single Thresholds: Int Multi-Component`, checks indices 0, 1 and 2. Legacy `Source/SIMPLib/CoreFilters/MultiThresholdObjects.cpp` and `MultiThresholdObjects2.cpp` contain the scalar-array checks in the SIMPL snapshot `0afaa6854`, selected as the last revision on or before the DREAM.3D `v6.5.171` tag date, 2026-05-27. --- -## MultiThresholdObjectsFilter-D2 +### MultiThresholdObjectsFilter-D5 | Field | Value | |---|---| -| **Deviation ID** | `MultiThresholdObjectsFilter-D2` | +| **Deviation ID** | `MultiThresholdObjectsFilter-D5` | | **Filter UUID** | `4246245e-1011-4add-8436-0af6bed19228` | -| **Status** | active in all releases through **7.4.1**; resolved in PR #1688 (2026-08-20) — resolved in DREAM3D-NX **7.4.2** | +| **Status** | active | -**Symptom:** On `develop`, a leaf combined with an inverted nested set (`AB3`: `{leaf: Int32 < 80, invertedNestedSet: NOT(Int32 > 30 AND Float32 < 0.95)}`) produced incorrect mask output. Quantified on the `AB3` fixture: **51 of 100 values differ** vs. legacy `Threshold Objects (Advanced)` and the numpy oracle. `AB3`'s shape overlaps with `D1`'s mixed-leaf/nested-set trigger, so this result is not a clean isolation of the inversion defect alone — both mechanisms plausibly contribute to the discrepancy. +**Symptom:** SIMPLNX offers selectable mask types and defaults to uint8. DREAM.3D v6.5.171 always creates a boolean mask. -**Root cause:** Bug (SIMPLNX-side, `develop`). Per the reporting engineer: legacy's `invertThreshold()` flips mask values element-wise; the `develop` code instead called `std::reverse()` on the intermediate result buffer under certain replace/invert conditions — reversing the *order* of elements rather than flipping each element's own TRUE/FALSE value. This is not a valid implementation of per-element boolean inversion. This PR fixed it by consolidating all inversion through a single, consistent per-element-flip combination path (visible today as `InsertThreshold`'s `newVector.getValue(i) != inverse`). The precise nesting depth at which the `develop` `std::reverse` branch was reachable (top-level only, or also for a nested child, as in `AB3`) was not independently re-derived line-by-line for this report; documented here on the `AB3` runtime evidence. +**Root cause:** algorithmic choice. `MultiThresholdObjectsFilter::parameters()` sets the default mask type to uint8. `FromSIMPLJson()` retains that default when `ScalarType` is absent. Both legacy filters create `DataArray` and expose no mask-type parameter. -**Affected users:** Any pipeline using an inverted `ArrayThresholdSet` — top-level or nested — combined with sibling thresholds/sets. Not a narrow edge case: "Invert Mask" is a standard, documented option. On an image geometry, wrong tuple correspondence scrambles which voxels are masked; there is no legitimate downstream use of the `develop` output. +**Affected users:** Users who convert a legacy pipeline or connect the mask to a downstream operation that requires a boolean array. Default values remain 1 and 0, but the declared array type differs. -**Regression coverage:** `Valid Threshold Sets` → `ArraySet 7: leaf + inverted nested set` (`CreateThresholdSet7`/`CheckThresholdSet7`), the in-repo analogue of the `AB3` shape, swept over `isInverted`. Verified load-bearing by the same stub experiment as D1. +**Recommendation:** either acceptable within zero tolerance for default mask values. Select boolean as the Mask Type when downstream operations require the legacy array type. -**Recommendation:** Trust SIMPLNX (confirmed bit-for-bit against legacy on `AB3` at both 100 tuples and 50M tuples). Anyone on a released version through 7.4.1 using an inverted threshold set should upgrade and re-verify any pipeline outputs generated before the fix. +**Evidence:** Current `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/MultiThresholdObjectsFilter.cpp` defines the default and conversion fallback. The `Valid Execution, Mask DataType` case in `src/Plugins/SimplnxCore/test/MultiThresholdObjectsVvTest.cpp` checks ten numeric output types; `Valid Execution` in `MultiThresholdObjectsTest.cpp` checks boolean output. Both legacy sources identified under D3 allocate boolean masks. --- -## MultiThresholdObjectsFilter-D3 +### MultiThresholdObjectsFilter-D6 | Field | Value | |---|---| -| **Deviation ID** | `MultiThresholdObjectsFilter-D3` | +| **Deviation ID** | `MultiThresholdObjectsFilter-D6` | | **Filter UUID** | `4246245e-1011-4add-8436-0af6bed19228` | | **Status** | active | -**Symptom:** SIMPLNX accepts a component index on a multi-component threshold array (`ArrayThreshold::setComponentIndex()`); neither legacy filter has an equivalent parameter. +**Symptom:** SIMPLNX can write custom TRUE and FALSE values. DREAM.3D v6.5.171 writes boolean values and has no custom-value option. + +**Root cause:** algorithmic choice. SIMPLNX adds separate enable flags and numeric values for TRUE and FALSE. Direct and Scanline use the selected values to represent matches and nonmatches. The legacy parameter lists contain no corresponding options. -**Root cause:** Algorithmic choice (deliberate SIMPLNX capability addition, not a port artifact — appropriate under this filter's Rewrite classification). Confirmed directly from legacy source: `MultiThresholdObjects2::dataCheck()` rejects non-scalar (multi-component) arrays outright with error `-11003`. Legacy `Threshold Objects` (the non-Advanced filter) has no per-component comparison concept either. SIMPLNX's `#1184` (`32837a30f`) added multi-component index selection with no legacy equivalent in either predecessor. +**Affected users:** Users who need numeric mask values other than 1 and 0. Default settings retain boolean mask values. -**Affected users:** Nobody migrating *from* legacy is affected (the capability didn't exist to lose). Anyone relying on this SIMPLNX-only feature should be aware there is no DREAM3D 6.5.171 equivalent pipeline to fall back to if downgrading. +**Recommendation:** trust SIMPLNX. Use the default values for legacy-compatible mask values; use custom values when the downstream operation requires them. -**Recommendation:** Trust SIMPLNX. This is an intentional superset capability, not a correctness issue. Worth a one-line callout in public migration guidance: legacy `Threshold Objects (Advanced)` never supported per-component thresholding on multi-component arrays; SIMPLNX does. +**Evidence:** `Valid Execution - Custom Values` in both `src/Plugins/SimplnxCore/test/MultiThresholdObjectsTest.cpp` and `MultiThresholdObjectsVvTest.cpp` checks TRUE=25 and FALSE=10 across ten numeric output types. Both legacy sources identified under D3 write boolean output. --- -## MultiThresholdObjectsFilter-D4 +## Part 2 — SIMPLNX defects and behavior changes + +### MultiThresholdObjectsFilter-D1 | Field | Value | |---|---| -| **Deviation ID** | `MultiThresholdObjectsFilter-D4` | +| **Deviation ID** | `MultiThresholdObjectsFilter-D1` | | **Filter UUID** | `4246245e-1011-4add-8436-0af6bed19228` | -| **Status** | active in all releases through **7.4.1**; resolved in PR #1688 (2026-08-20) — resolved in DREAM3D-NX **7.4.2** | - -**Symptom:** A legacy `Threshold Objects (Advanced)` pipeline whose thresholds contain a *nested* comparison set produced the wrong mask after conversion to SIMPLNX. The nesting was discarded and every union operator was reset to AND, so `A AND (B OR C)` was evaluated as `A AND B AND C`. Quantified on the `AB4`–`AB6` fixtures against real legacy output: **55/100, 50/100, and 4/100 tuples wrong** respectively. `AB6` degenerated to an all-false mask. Filters run from natively-authored SIMPLNX pipelines were never affected — only pipelines converted from SIMPL. - -**Root cause:** Bug (SIMPLNX-side, pre-existing on `develop`). Two independent defects in `src/simplnx/Parameters/ArrayThresholdsParameter.cpp`, both in the `ComparisonSelectionAdvancedFilterParameterConverter` path: - -1. `flattenSetThreshold()` collapsed each nested `ArrayThresholdSet` into its parent's flat threshold list. SIMPLNX represents nested sets natively, so the flattening served no purpose and destroyed the AND/OR grouping the legacy pipeline encoded. -2. `convertArrayThreshold()` never called `setUnionOperator()`, so every converted leaf silently took `ArrayThreshold`'s default (`And`). Any OR in the legacy pipeline was lost. This affected leaves at every level, not just nested ones. +| **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 | -The flattening pass also carried a third latent defect: it attempted to push an inverted set's inversion down onto its leaves by flipping each comparison operator, which is not a valid negation — `NOT(x > v)` is `x >= v`, not `x < v`, so tuples exactly equal to the threshold were misclassified; De Morgan's required AND↔OR flip was never applied; and `Operator_NotEqual` fell through unchanged, so `NOT(x != v)` stayed `x != v`. +**Symptom:** A nested threshold set could overwrite valid mask values with false. A leaf followed by an AND-connected nested set could produce an all-false mask. -**Fix:** the converter now keeps the nested `ArrayThresholdSet` (which `convertSetThreshold()` was already building correctly) and sets each leaf's union operator from the legacy `"Union Operator"` key. `flattenSetThreshold()` and `invertComparison()` are removed. Set inversion is handled natively by the algorithm, which is where it is now correctly applied. +**Root cause:** bug. `ThresholdSet()` allocated a false-filled temporary vector but evaluated its children into the parent output store. It then copied or combined the untouched temporary vector into that same output. The current Direct and Scanline evaluators give each child a separate result and combine it into its parent. DREAM.3D v6.5.171 evaluated children into a separate set buffer and was not affected. -**Affected users:** Anyone opening or running a converted DREAM3D 6.5 `Threshold Objects (Advanced)` pipeline that used a nested comparison set or any OR union — a standard use of that filter, and the only reason to choose the Advanced variant over the basic one. The failure was silent: the filter reported success and wrote a wrong mask. Natively-authored SIMPLNX pipelines were never affected. +**Affected users:** Users of DREAM3D-NX v7.0.0 through v7.4.1 whose threshold trees contain nested sets, including mixed leaf and nested-set siblings. -**Regression coverage:** `SIMPL Nested Set Conversion` (`test/MultiThresholdObjectsTest.cpp`, fixture `test/simpl_conversion/6_5/MultiThresholdObjectsFilter_Nested.json`) asserts that a converted leaf-plus-inverted-nested-set survives as a nested `ArrayThresholdSet` with its inversion flag and its children's union operators intact. Verified load-bearing: the test fails against the pre-fix converter. +**Recommendation:** trust SIMPLNX. Use DREAM3D-NX v7.4.2 or later and regenerate masks produced with affected nested trees. -**Recommendation:** Trust SIMPLNX (confirmed against legacy on `AB4`–`AB6`). Any pipeline converted from SIMPL by a released version through 7.4.1 that used nested sets or OR unions should be re-converted and re-run; masks produced from such a converted pipeline should not be trusted. +**Evidence:** `git show` inspection of all ten release sources places the defect in v7.0.0 through v7.4.1 and the correction in v7.4.2. The affected code is in `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/MultiThresholdObjectsFilter.cpp` through v7.4.0 and `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/MultiThresholdObjects.cpp` in v7.4.1. PR #1688 (`467a250d1`) supplies the correction. `src/Plugins/SimplnxCore/test/MultiThresholdObjectsVvTest.cpp`, `Valid Threshold Sets` / `ArraySet 6: leaf + nested set`, checks the corrected mask. Legacy `Source/SIMPLib/CoreFilters/MultiThresholdObjects2.cpp`, `thresholdSet()`, in SIMPL snapshot `0afaa6854` uses its set buffer for child evaluation. --- -## MultiThresholdObjectsFilter-D5 +### MultiThresholdObjectsFilter-D2 | Field | Value | |---|---| -| **Deviation ID** | `MultiThresholdObjectsFilter-D5` | +| **Deviation ID** | `MultiThresholdObjectsFilter-D2` | | **Filter UUID** | `4246245e-1011-4add-8436-0af6bed19228` | | **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:** SIMPLNX can write the mask with a user-selected `DataType` and defaults to `uint8`. Both legacy filters always created the mask as `DataArray` with no output-type option, so a converted 6.5 pipeline produces a `uint8` mask where legacy produced a `bool` mask. The mask *values* are identical (1/0); only the array's type differs. +**Symptom:** Inverted threshold trees could select the wrong tuples. Reversing tuple order did not compute the logical complement of each value. -**Root cause:** Algorithmic choice (deliberate SIMPLNX capability addition). Confirmed directly from legacy source: `MultiThresholdObjects::dataCheck()` and `MultiThresholdObjects2::dataCheck()` both call `createNonPrereqArrayFromPath, AbstractFilter, bool>(...)` unconditionally. SIMPLNX added the `Mask Type` parameter with a `uint8` default (`#1502`). `FromSIMPLJson()` maps the SIMPL `ScalarType` key when present, but 6.5 pipelines do not carry that key, so a converted 6.5 pipeline falls through to the SIMPLNX default rather than to `boolean`. +**Root cause:** bug. The replace-and-invert paths called `std::reverse()` instead of flipping each mask value. Recursive calls also passed false for inversion, and the root inversion flag was passed to individual children before combination. The current evaluators apply each node's inversion after that node is complete. DREAM.3D v6.5.171 used element-wise `invertThreshold()`, honored nested inversion and inverted the completed root; it was not affected. -**Affected users:** Anyone converting a 6.5 pipeline whose downstream filters require a `boolean` mask array, or who compares a converted pipeline's output file against a legacy one and finds the mask array typed `uint8` instead of `bool`. Numerically the masks agree, so results are affected only where the array's declared type matters. +**Affected users:** Users of DREAM3D-NX v7.0.0 through v7.4.1 who invert a root threshold set or a nested set. -**Recommendation:** Either acceptable. Users who need bit-for-bit legacy parity on the output file should set **Mask Type** to `boolean` explicitly after converting a 6.5 pipeline. Worth a line in public migration guidance. +**Recommendation:** trust SIMPLNX. Use DREAM3D-NX v7.4.2 or later and regenerate masks made with inverted threshold trees. + +**Evidence:** All release sources through v7.4.1 contain `std::reverse()` in the threshold evaluator at the paths identified under D1. The v7.4.2 source applies logical inversion in `InsertThreshold()` and at the final root write. `src/Plugins/SimplnxCore/test/MultiThresholdObjectsVvTest.cpp`, `Valid Threshold Sets` / `ArraySet 7: leaf + inverted nested set`, checks nested inversion. `Top-level inversion is logical` and `Nested sets compose before root inversion` in `src/Plugins/SimplnxCore/test/MultiThresholdObjectsTest.cpp` check the root. Legacy `MultiThresholdObjects2.cpp` in SIMPL snapshot `0afaa6854` contains the corresponding logical inversion paths. --- -## MultiThresholdObjectsFilter-D6 +### MultiThresholdObjectsFilter-D4 | Field | Value | |---|---| -| **Deviation ID** | `MultiThresholdObjectsFilter-D6` | +| **Deviation ID** | `MultiThresholdObjectsFilter-D4` | | **Filter UUID** | `4246245e-1011-4add-8436-0af6bed19228` | | **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:** SIMPLNX can substitute custom TRUE and FALSE values into the mask instead of 1 and 0 (`Use Custom TRUE Value` / `Use Custom FALSE Value`). Neither legacy filter has any equivalent parameter. - -**Root cause:** Algorithmic choice (deliberate SIMPLNX capability addition, `#669`). Confirmed directly from legacy source: `MultiThresholdObjects::setupFilterParameters()` declares exactly two parameters — `SelectedThresholds` and `DestinationArrayName` — and `MultiThresholdObjects2` likewise has no custom-value parameter. Both write a hard-coded `true`/`false` into a `DataArray`. - -**Affected users:** Nobody migrating *from* legacy is affected — the capability did not exist to lose, and the defaults (1/0) reproduce legacy behavior exactly. Anyone relying on this SIMPLNX-only feature should know there is no DREAM3D 6.5.171 equivalent pipeline to fall back to. +**Symptom:** Converting an Advanced legacy pipeline could discard its grouping and OR operations, so the converted pipeline computed a different mask. -**Recommendation:** Trust SIMPLNX. Intentional superset capability, not a correctness issue. Because the custom values are substituted only at the final typed write, after the boolean combination is complete, they cannot affect which tuples are masked — the `Valid Execution - Custom Values` `TEMPLATE_TEST_CASE` covers all 10 numeric mask types analytically. +**Root cause:** bug. `convertArrayThreshold()` did not copy the leaf union operator. `flattenSetThreshold()` removed nested groups and tried to negate comparisons by swapping operators. That transformation lost equality boundaries and did not apply De Morgan's rule. The converter now retains nested sets, their inversion flags and each leaf's union operator. DREAM.3D v6.5.171 was not affected because this SIMPLNX conversion path does not exist in the legacy application. -## Outstanding comparison work +**Affected users:** Users who converted Advanced legacy pipelines with nested sets or OR operations in DREAM3D-NX v7.0.0 through v7.4.1. Native SIMPLNX pipelines did not use this converter. -**None.** Both legacy filters have been run separately on representative configurations (`AB1` vs. `Threshold Objects`; `AB2`–`AB6` vs. `Threshold Objects (Advanced)`), satisfying this filter's Rewrite-classification requirement that functional equivalence be confirmed against both predecessors. The three items previously listed here were closed at second-engineer review on 2026-08-19: +**Recommendation:** trust SIMPLNX. Convert the original legacy pipeline again with DREAM3D-NX v7.4.2 or later, then regenerate its masks. -1. **Custom TRUE/FALSE mask output values** — closed by inspection of the legacy sources rather than by an A/B run: neither legacy filter has the parameter, so there is nothing to compare against. Recorded as `MultiThresholdObjectsFilter-D6`. -2. **Default mask output `DataType`** — closed the same way: both legacy filters hard-code `DataArray`. Recorded as `MultiThresholdObjectsFilter-D5`, with migration guidance. -3. **A broader configuration sweep** — closed by running `AB4`–`AB6` (three levels of nesting, an inverted sibling set, and a grouping-sensitive case), all MATCH. This sweep is also what surfaced `MultiThresholdObjectsFilter-D4`. +**Evidence:** `src/simplnx/Parameters/ArrayThresholdsParameter.cpp` in every release through v7.4.1 contains `flattenSetThreshold()` and omits the leaf `setUnionOperator()` call. The v7.4.2 source preserves both. PR #1688 (`467a250d1`) contains the fix. `src/Plugins/SimplnxCore/test/MultiThresholdObjectsVvTest.cpp`, `SIMPL Nested Set Conversion`, checks the tree from `src/Plugins/SimplnxCore/test/simpl_conversion/6_5/MultiThresholdObjectsFilter_Nested.json`. All filter tests pass in CI at `419f9db35` on Linux, macOS and Windows in-core builds. From bc22332e7b1e9fe0e96688b613a4f0a217c72a39 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 20:01:29 -0400 Subject: [PATCH 57/91] DOC: Rewrite the RegularizeZSpacing V&V record to follow the report template Signed-off-by: Michael Jackson --- .../vv/RegularizeZSpacingFilter.md | 140 +++++++++--------- .../vv/deviations/RegularizeZSpacingFilter.md | 65 ++++++-- 2 files changed, 123 insertions(+), 82 deletions(-) diff --git a/src/Plugins/SimplnxCore/vv/RegularizeZSpacingFilter.md b/src/Plugins/SimplnxCore/vv/RegularizeZSpacingFilter.md index 7358371069..961a11d652 100644 --- a/src/Plugins/SimplnxCore/vv/RegularizeZSpacingFilter.md +++ b/src/Plugins/SimplnxCore/vv/RegularizeZSpacingFilter.md @@ -1,106 +1,110 @@ # V&V Report: RegularizeZSpacingFilter -| | | -|-----------|------------------| -| Plugin | SimplnxCore | +| | | +|--------|--------------| +| Plugin | SimplnxCore | | SIMPLNX UUID | `d6599986-1932-4bfc-993d-71eafefe6db0` | -| DREAM3D 6.5.171 equivalent | `RegularizeZSpacing` — `Source/Plugins/Sampling/SamplingFilters/RegularizeZSpacing.{h,cpp}` (legacy UUID `bc4952fa-34ca-50bf-a1e9-2b9f7e5d47ce`) | +| SIMPLNX Human Name | Regularize Z Spacing | +| DREAM.3D v6.5.171 equivalent | `RegularizeZSpacing` (Sampling plugin, UUID `bc4952fa-34ca-50bf-a1e9-2b9f7e5d47ce`) | | Verified commit | ** | | Status | COMPLETE | -| Sign-off | *Michael Jackson (port + V&V cycle, 2026-07-08); | +| Sign-off | Michael Jackson, 2026-07-08; second engineer: Jared Duffey, 2026-07-14 | ## At a glance -| Aspect | Current state | -|------------------------|--------------------------| -| Algorithm Relationship | **Port** of legacy `RegularizeZSpacing::execute()` — identical Z-plane mapping rule and `floor(extent/newZRes)` dim math (independently hand-traced, including the strict-`>` boundary and clamp cases). Five port-time deltas (bulk copy, new-geometry output mode, preflight validation, parallelization, cell-AM binding); none change output for valid input. | -| Oracle (confirmed) | **Class 1 (Analytical)** primary + **Class 4 (Invariant)** companion — closed-form indirection map `out[i] = in[map[i]]`. Element-wise Class 1 assertions in 2 fixtures (`Valid Execution (New Geometry)`, `Valid Execution (Spacing Exceeds Extent)`); Class 4 invariants across the valid fixtures. All pass in-core + OOC. | -| Code paths enumerated | 13 of 15 exercised; the 2 uncovered are the redundant file-open guard and the cancel check (reasons below). | -| Tests today | 5 registered cases plus one hidden HDF5 boundary test. Paired CTest selections pass 5/5. | -| Exemplar archive | **None** — inline analytical fixtures (no `download_test_data`; avoids a circular oracle per project policy). | -| Legacy comparison | **Run** vs DREAM3D 6.5.171 on a synthetic multi-type fixture (int32 + bool + 3-component float). Bit-identical: legacy == SIMPLNX == Class-1 oracle on every array and on geometry (dims/spacing/origin). | -| Bug flags | None. | -| 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 `RegularizeZSpacing` in DREAM.3D v6.5.171. The plane mapping is unchanged; SIMPLNX adds validation and an optional separate output geometry. | +| Oracle (confirmed) | Class 1 plane maps and Class 4 geometry invariants are encoded in `test/RegularizeZSpacingTest.cpp`. All pass in CI. | +| Code paths enumerated | 13 of 22 exercised by the ordinary tests. Gaps include defensive guards, cancellation, grouped child copying and additional array types. | +| Tests today | 6 test cases: 3 valid executions, 1 input-validation case, 1 SIMPL conversion case and 1 hidden HDF5 case. The five ordinary cases cover analytical values, geometry properties and error codes. | +| Exemplar archive | None. The test source constructs inputs and defines analytical expected values. | +| Legacy comparison | Not run — comparison artifacts are not available. | +| Bug flags | None | +| V&V phase | Analytical checks, input-validation checks and second-engineer review are complete. | ## Summary -`RegularizeZSpacingFilter` resamples an Image Geometry with irregular Z slice spacing onto a uniform Z spacing: it reads the physical Z position of each original slice boundary from a text file (`ZPoints + 1` values), computes `newZ = floor(totalZExtent / newZSpacing)` (min 1), and copies each new plane's cell data from the original plane whose boundary interval contains it. Verification used a **Class 1 (Analytical)** oracle — the algorithm is a closed-form indirection lookup `output[i] = input[map[i]]` — with **Class 4 (Invariant)** companions (X/Y dims and spacing preserved, origin preserved, Z spacing becomes the requested value). A direct A/B against DREAM3D 6.5.171 on a synthetic multi-type dataset is **bit-identical** (0 deviations); legacy, SIMPLNX, and the analytical oracle agree exactly. +`RegularizeZSpacingFilter` copies image planes onto a uniform Z spacing from a file of slice-boundary positions. Class 1 fixtures check exact output values, and Class 4 checks cover geometry properties and loose child data. All pass in CI. ## Algorithm Relationship -*Classification:* **Port** ~~| Minor changes | Rewrite | New filter~~ +*Classification:* Port -*Evidence:* Near line-by-line translation of legacy `RegularizeZSpacing::execute()`. Same SIMPL UUID retained and mapped in `SimplnxCoreLegacyUUIDMapping.hpp`. The new→old Z-plane mapping loop (`plane` = largest `iter` in `[1, origZ)` with `i*newZRes > zBound[iter]`, else 0) and the `newZ = (size_t)(lastZBound / newZRes)` (min 1) computation are reproduced exactly; an independent hand-trace confirmed the arithmetic types match legacy expression-by-expression (float32 throughout, no double promotion), so even float-boundary cases agree. - -*Port-time deltas (none change output for valid input — confirmed bit-identical in the legacy A/B):* - -1. **Data copy**: legacy per-tuple `memcpy` via a rebuilt `newindicies` table → SIMPLNX bulk `AbstractDataStore::copyFrom` of one contiguous plane-slab per destination plane. Same bytes moved; more efficient and out-of-core friendly. -2. **Output mode**: legacy replaces the cell AttributeMatrix in place → SIMPLNX uses the modern new-geometry pattern (`CreateImageGeometryAction` + rename + deferred delete) with a "Perform In Place" toggle (default true). In-place mode is behaviorally equivalent to legacy. -3. **Preflight validation added**: positive `new_z_spacing`; input file has ≥ `ZPoints + 1` parseable values; values monotonically non-decreasing; positive total Z extent; only DataArray members in the cell AM. These reject bad input that legacy silently consumed (short files reused the last-read value; non-monotonic files produced a garbage mapping; zero extent produced a clamped 1-plane output) but do not alter valid-input output. The `.txt` extension is a dialog hint only (`acceptAllExtensions`), so converted legacy pipelines referencing `.dat`/`.csv`/extensionless files still validate. -4. **Parallelization**: per-array parallel task runner (one task per cell array, distinct stores) vs legacy serial loop. No output effect. -5. **Cell-AM binding**: legacy took an explicit AttributeMatrix path; SIMPLNX takes the geometry and uses its assigned cell AttributeMatrix. `FromSIMPLJson` keeps only the DataContainer name from the legacy `CellAttributeMatrixPath` (the AM name is discarded). For the overwhelmingly common single-cell-AM case this is identity; corner-case consequences for converted pipelines are documented in the deviations file's scope note. Related accept/reject divergences: a geometry with no cell AM now errors (`-5560`, impossible to express in legacy), and a stale AM name that legacy rejected converts to the geometry's actual cell AM and proceeds. - -*Material PRs since baseline:* new filter port (this branch); no prior history. +*Evidence:* `src/Plugins/SimplnxCore/src/SimplnxCore/SimplnxCoreLegacyUUIDMapping.hpp` maps the legacy UUID to this filter; both implementations use the strict `>` plane-boundary comparison and truncate the output Z dimension with a minimum of one plane. ## Oracle -*Class:* **1 (Analytical)** primary + **4 (Invariant)** companion. - -*Applied (Class 1):* The output cell data is a closed-form indirection lookup `output_tuple[t] = input_tuple[map[t]]`, where `map` is derived by hand from the Z-positions file and `new_z_spacing`. Primary fixture (origZ=4, zBound `{0,1,3,6,10}`, newZRes 2.0): new→old plane map `[0,1,2,2,3]`, giving a fully hand-derived 10-tuple expected output. Clamp fixture (same bounds, newZRes 20.0): `newZ = floor(10/20) = 0 → 1`, expected output = source plane 0. Both derivations are embedded as comments beside the `REQUIRE`s. +*Class:* 1 (Analytical), with 4 (Invariant) companions. -*Applied (Class 4):* X and Y dimensions and spacing preserved, origin preserved, Z spacing equals `new_z_spacing`, `newZ == max(1, floor(lastZBound / newZRes))`, and loose (non-cell-AM) children are carried over intact. Asserted in the valid-execution fixtures. +*Applied:* Bounds `[0,1,3,6,10]` give source-plane map `[0,1,2,2,3]` at spacing 2 and `[0]` at spacing 20; invariant checks require preserved X/Y dimensions and spacing, origin, and loose child values. -*Encoded:* `test/RegularizeZSpacingTest.cpp::SimplnxCore::RegularizeZSpacingFilter: Valid Execution (New Geometry)` (element-wise Class 1 + invariants), `...::Valid Execution (Spacing Exceeds Extent)` (clamp-path Class 1), `...::Valid Execution (In Place)` (in-place invariants) — all pass in-core (`NX-Com-Qt69-Vtk95-Rel`) and out-of-core (`NX-OOC-Qt69-Vtk95-Rel`). +*Encoded:* `src/Plugins/SimplnxCore/test/RegularizeZSpacingTest.cpp::SimplnxCore::RegularizeZSpacingFilter: Valid Execution (New Geometry)` and `Valid Execution (Spacing Exceeds Extent)` contain 2 analytical fixtures; `Valid Execution (In Place)` adds geometry checks. All pass in CI. -*Second-engineer review:* **Pending.** The oracle is a deterministic index remap (lowest-risk oracle class); review still recommended before promotion to COMPLETE. +*Second-engineer review:* Jared Duffey, 2026-07-14. ## 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. +None. ## Code path coverage -`13 of 15 paths exercised.` The 2 uncovered are a redundant file-open guard and the cancel check, listed with reasons. - -Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/RegularizeZSpacing.cpp` (217 lines) + `Filters/RegularizeZSpacingFilter.cpp` (309 lines, preflight). Logical phases: **(a) preflight validation**, **(b) output-geometry construction**, **(c) mapping + copy**. - -| # | Phase | Path | Test case | -|----|-------|------|-----------| -| 1 | (a) | `new_z_spacing <= 0` → error `-5555` | `Invalid Parameters` / "Non-positive Z spacing" (code asserted) | -| 2 | (a) | input file cannot be opened → error `-5556` | *Not directly tested. File existence is validated by the `FileSystemPathParameter` before preflight; this is a redundant safety net reachable only by a filesystem race.* | -| 3 | (a) | file has fewer than `ZPoints + 1` parseable values → error `-5557` | `Invalid Parameters` / "File with too few values" (code asserted) | -| 4 | (a) | Z boundary values not monotonically non-decreasing → error `-5558` | `Invalid Parameters` / "Non-monotonic Z boundary values" (code asserted) | -| 5 | (a) | total Z extent (last value) `<= 0` → error `-5559` | `Invalid Parameters` / "Zero total Z extent" (code asserted) | -| 6 | (b) | geometry has no cell AttributeMatrix → error `-5560` | `Invalid Parameters` / "Missing cell Attribute Matrix" (code asserted) | -| 7 | (b) | cell AM member is not a DataArray → error `-5561` | `Invalid Parameters` / "Non-DataArray member in cell Attribute Matrix" (code asserted) | -| 8 | (b) | `remove_original_geometry == true` → rename src, create temp geom, deferred delete + rename | `Valid Execution (In Place)` | -| 9 | (b) | `remove_original_geometry == false` → create new geometry | `Valid Execution (New Geometry)` | -| 10 | (b) | copy loose child data objects from source geometry (component-wise path rebase) | `Valid Execution (New Geometry)` — `LooseData` presence + values asserted | -| 11 | (c) | `newZ == 0` → clamp to 1 (`ComputeRegularizedZDim`) | `Valid Execution (Spacing Exceeds Extent)` — Class 1 expected output asserted | -| 12 | (c) | build `newToOldZPlane` mapping (nearest-below, strict `>`) | `Valid Execution (New Geometry)` — verified against oracle map `[0,1,2,2,3]` | -| 13 | (c) | per-array bulk `copyFrom` of each plane slab | all three valid fixtures + legacy A/B (int32/bool/float3) | -| 14 | (c) | cancel check (per-array, per-plane) | *Not directly tested. Requires cancel-signal injection; branch is a simple early return.* | -| 15 | OOC boundary | slice-copy oracle | `real HDF5 slice-copy oracle` — independent expected outputs on actual HDF5 stores | +13 of 22 paths exercised. Each uncovered row states the missing fixture or required injection. + +Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/RegularizeZSpacing.cpp` (208 lines), `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/RegularizeZSpacingFilter.cpp` (310 lines), and `src/simplnx/Utilities/FilterUtilities.cpp` (156 lines, copied-AttributeMatrix warning). + +Test names below omit the common `SimplnxCore::RegularizeZSpacingFilter: ` prefix. + +| # | Phase | Path | Test case | +|---|-------|------|-----------| +| 1 | Preflight | Non-positive Z spacing returns `-5555` | `Invalid Parameters` / `Non-positive Z spacing` | +| 2 | Preflight | Boundary file cannot be opened; return `-5556` | *Not directly tested. Requires a file-open failure after parameter validation or a direct helper call.* | +| 3 | Preflight | Fewer than `ZPoints + 1` parseable values return `-5557` | `Invalid Parameters` / `File with too few values` | +| 4 | Preflight | Decreasing boundary values return `-5558` | `Invalid Parameters` / `Non-monotonic Z boundary values` | +| 5 | Preflight | Non-positive final boundary returns `-5559` | `Invalid Parameters` / `Zero total Z extent` | +| 6 | Geometry | Missing assigned cell AttributeMatrix returns `-5560` | `Invalid Parameters` / `Missing cell Attribute Matrix` | +| 7 | Geometry | Non-DataArray cell member returns `-5561` | `Invalid Parameters` / `Non-DataArray member in cell Attribute Matrix` | +| 8 | Geometry | In-place mode replaces the source geometry through deferred actions | `Valid Execution (In Place)` checks the resulting name, Z dimension and spacing | +| 9 | Geometry | Separate-output mode retains the source geometry | `Valid Execution (New Geometry)` checks geometry properties and all output cell values | +| 10 | Geometry | Copy a loose array with its values intact | `Valid Execution (New Geometry)` checks all four `LooseData` values | +| 11 | Mapping | Output Z dimension truncates to zero; clamp to one plane | `Valid Execution (Spacing Exceeds Extent)` checks dimensions and values `[0,1]` | +| 12 | Mapping | Strict `>` comparison selects source planes, including an exact boundary | `Valid Execution (New Geometry)` checks map `[0,1,2,2,3]` | +| 13 | Copy | Copy each mapped int32 plane as a contiguous slab | `Valid Execution (New Geometry)` and `Valid Execution (Spacing Exceeds Extent)` check every output value | +| 14 | Cancellation | Stop before an array or before a plane copy | *Not directly tested. Requires cancellation injection.* | +| 15 | Copy | Copy planes and loose data through HDF5 stores | *Not directly tested. Requires the hidden `real HDF5 slice-copy oracle`; the cited CI evidence covers in-memory execution.* | +| 16 | Execution | Propagate an error when the boundary file is read again | *Not directly tested. Requires a file change between preflight and execution or a direct algorithm call.* | +| 17 | Execution | Reject a non-DataArray cell member with `-5561` | *Not directly tested. Preflight rejects the same member before execution.* | +| 18 | Geometry | Copy a loose group and rebase descendant paths | *Not directly tested. The fixtures contain a loose array, not a loose group.* | +| 19 | Preflight | Warn `-4015` when an additional AttributeMatrix is copied unchanged | *Not directly tested. No fixture contains an additional AttributeMatrix.* | +| 20 | Copy | Dispatch boolean and other numeric types, or copy multiple components | *Not directly tested. The fixtures use scalar int32 arrays.* | +| 21 | Parameters | Accept a readable boundary file without a `.txt` extension | *Not directly tested. Every boundary-file fixture uses `.txt`.* | +| 22 | Conversion | Convert legacy geometry selection, file, spacing and in-place default | `SIMPL Backwards Compatibility` / `SIMPL 6.5 (UUID)` and `SIMPL 6.4 (Filter_Name)` | ## Test inventory +All six cases are in `src/Plugins/SimplnxCore/test/RegularizeZSpacingTest.cpp`. The five ordinary cases run in Linux, macOS and Windows in-core builds. All pass in CI. + | Test case | Status | Notes | |-----------|--------|-------| -| `Valid Execution (New Geometry)` | new-for-V&V | Class 1 oracle: asserts output Z dim (5), preserved X/Y dims + spacing, preserved origin, Z spacing = 2.0, the 10-tuple expected `Data` array element-wise, loose-child (`LooseData`) copy with values, and source-geometry retention. | -| `Valid Execution (In Place)` | new-for-V&V | Asserts in-place result replaces the source geometry (same name), no output-name geometry created, output Z dim + Z spacing correct. | -| `Valid Execution (Spacing Exceeds Extent)` | new-for-V&V | Class 1 clamp fixture: `newZRes` (20) > extent (10) → 1 output plane sourced from plane 0; dims (2,1,1), Z spacing 20, `Data` = {0,1} asserted. | -| `Invalid Parameters` | new-for-V&V | 6 SECTIONs — non-positive spacing (`-5555`), too-few file values (`-5557`), non-monotonic values (`-5558`), zero total extent (`-5559`), missing cell AM (`-5560`), non-DataArray cell member (`-5561`); each asserts the specific error code. | -| `SIMPL Backwards Compatibility` | new-for-V&V | 2 DYNAMIC_SECTIONs — converts the 6.5 (UUID-keyed) and 6.4 (Filter_Name-keyed) fixtures in `test/simpl_conversion/`; asserts converted geometry path, input file, Z spacing, and the in-place default. The 6.4 path exercises the `k_LegacySimplFilterUuidMap` name resolution. | -| `real HDF5 slice-copy oracle` | new-for-V&V | 67 assertions; The existing source fixture and Z-boundary writer produce the analytical source-plane map [0,1,2,2,3]. HDF5 input and output stores preserve [0,1,2,3,4,5,4,5,6,7] across plane-copy boundaries. In-place and new-geometry modes both pass, including the loose child array. | - -OOC recertification (2026-09-21): serial CTest passed 5/5 target entries in each DREAM3DNX build. The hidden `real HDF5 slice-copy oracle` passed 67 assertions in the OOC binary. The existing source fixture and Z-boundary writer produce the analytical source-plane map [0,1,2,2,3]. HDF5 input and output stores preserve [0,1,2,3,4,5,4,5,6,7] across plane-copy boundaries. In-place and new-geometry modes both pass, including the loose child array. 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. +| `SimplnxCore::RegularizeZSpacingFilter: Valid Execution (New Geometry)` | new-for-V&V | Checks all 10 output values against `[0,1,2,3,4,5,4,5,6,7]`, geometry dimensions, spacing, origin, four loose child values and source retention. | +| `SimplnxCore::RegularizeZSpacingFilter: Valid Execution (Spacing Exceeds Extent)` | new-for-V&V | Checks the one-plane minimum at spacing 20, output dimensions `(2,1,1)` and values `[0,1]`. | +| `SimplnxCore::RegularizeZSpacingFilter: Valid Execution (In Place)` | new-for-V&V | Checks replacement under the original name, absence of the separate output name, Z dimension 5 and Z spacing 2. Does not assert cell values. | +| `SimplnxCore::RegularizeZSpacingFilter: Invalid Parameters` | new-for-V&V | Six sections check exact codes `-5555`, `-5557`, `-5558`, `-5559`, `-5560` and `-5561`. | +| `SimplnxCore::RegularizeZSpacingFilter: real HDF5 slice-copy oracle` | new-for-V&V | Hidden. Requires HDF5 input and output stores; both in-place settings check all 10 cell values, four loose child values and tuple dimensions. | +| `SimplnxCore::RegularizeZSpacingFilter: SIMPL Backwards Compatibility` | new-for-V&V | Dynamic sections `SIMPL 6.5 (UUID)` and `SIMPL 6.4 (Filter_Name)` check the filter UUID, geometry path, input file, spacing and in-place default. | ## Exemplar archive - **Archive:** None — inline analytical fixtures. -- **SHA512:** N/A (no `download_test_data`). -- **Provenance:** N/A — Class 1/4 oracles live in the test source directly; no exemplar archive to provenance. +- **SHA512:** Not applicable. `src/Plugins/SimplnxCore/test/CMakeLists.txt` registers no archive for this filter. +- **Provenance:** Not applicable. The Class 1 and Class 4 oracles are in `src/Plugins/SimplnxCore/test/RegularizeZSpacingTest.cpp`. + +## Deviations from DREAM.3D v6.5.171 -## Deviations from DREAM3D 6.5.171 +See `vv/deviations/RegularizeZSpacingFilter.md` for the source comparison and migration guidance. These Part 1 entries describe parameter and validation differences. -No deviations observed. Comparison run on a synthetic Image Geometry (dims 3×2×4; int32 `Data`, `DataArray` `Mask`, 3-component float `Vec`) authored in legacy v7 format so both runners read identical input; Z-positions `{0,1,3,6,10}`, `new_z_spacing = 2.0`. Legacy 6.5.171 output, SIMPLNX output, and the Class-1 oracle are bit-identical on every cell array and on geometry (dimensions, spacing, origin). A conversion-semantics scope note (cell-AM binding, delta 5) is recorded in `vv/deviations/RegularizeZSpacingFilter.md`. +| Deviation | Observed difference | +|-----------|---------------------| +| `RegularizeZSpacingFilter-D1` | SIMPLNX selects the geometry's assigned cell AttributeMatrix; legacy conversion discards the explicitly selected AttributeMatrix name. | +| `RegularizeZSpacingFilter-D2` | SIMPLNX rejects incomplete or nonnumeric boundary files, decreasing boundaries and a non-positive final boundary. DREAM.3D v6.5.171 lacks these checks. | +| `RegularizeZSpacingFilter-D3` | SIMPLNX can retain the input geometry and create a separate output. DREAM.3D v6.5.171 replaces the selected cell data in place. | diff --git a/src/Plugins/SimplnxCore/vv/deviations/RegularizeZSpacingFilter.md b/src/Plugins/SimplnxCore/vv/deviations/RegularizeZSpacingFilter.md index 1211f9eac2..9bc18738af 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/RegularizeZSpacingFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/RegularizeZSpacingFilter.md @@ -1,28 +1,65 @@ -# Deviations from DREAM3D 6.5.171: RegularizeZSpacingFilter +# Deviations from DREAM.3D v6.5.171: RegularizeZSpacingFilter -This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent (`RegularizeZSpacing`, Sampling plugin, UUID `bc4952fa-34ca-50bf-a1e9-2b9f7e5d47ce`). +Part 1 records differences confirmed by source comparison with `RegularizeZSpacing` in the Sampling plugin. Entries use stable IDs from one sequence. The Filter UUID is the permanent cross-reference anchor. -Entries are referenced by stable ID (`RegularizeZSpacingFilter-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 + +### RegularizeZSpacingFilter-D1 + +| Field | Value | +|---|---| +| **Deviation ID** | `RegularizeZSpacingFilter-D1` | +| **Filter UUID** | `d6599986-1932-4bfc-993d-71eafefe6db0` | +| **Status** | active | + +**Symptom:** A converted pipeline selects the geometry's assigned cell AttributeMatrix, even if its legacy parameter names another AttributeMatrix. + +**Root cause:** Algorithmic choice. DREAM.3D v6.5.171 selects cell data through the full `CellAttributeMatrixPath`. SIMPLNX selects an Image Geometry and reads its assigned cell AttributeMatrix. `FromSIMPLJson()` keeps only the DataContainer name from the legacy path. With one matching cell AttributeMatrix, this preserves the selection. With multiple cell AttributeMatrices, it can select different data. A stale AttributeMatrix name that DREAM.3D rejects does not survive conversion. SIMPLNX returns `-5560` when the geometry has no assigned cell AttributeMatrix and `-5561` for a non-DataArray cell member. + +**Affected users:** Users who convert pipelines with multiple cell AttributeMatrices or a stale AttributeMatrix name. + +**Recommendation:** Trust SIMPLNX after checking the converted selection. Assign the intended cell AttributeMatrix to the selected Image Geometry before execution. + +**Evidence:** `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/RegularizeZSpacingFilter.cpp` uses `DataArraySelectionToGeometrySelectionFilterParameterConverter`, whose implementation in `src/simplnx/Parameters/GeometrySelectionParameter.cpp` retains only the DataContainer name. DREAM.3D v6.5.171 `Source/Plugins/Sampling/SamplingFilters/RegularizeZSpacing.cpp` validates and uses the full AttributeMatrix path. `SIMPL Backwards Compatibility` in `src/Plugins/SimplnxCore/test/RegularizeZSpacingTest.cpp` checks the converted geometry path. --- -## No deviations +### RegularizeZSpacingFilter-D2 + +| Field | Value | +|---|---| +| **Deviation ID** | `RegularizeZSpacingFilter-D2` | +| **Filter UUID** | `d6599986-1932-4bfc-993d-71eafefe6db0` | +| **Status** | active | + +**Symptom:** SIMPLNX rejects incomplete or nonnumeric boundary files, decreasing boundaries and a non-positive final boundary that DREAM.3D v6.5.171 does not explicitly reject. + +**Root cause:** Algorithmic choice. SIMPLNX checks every requested extraction and returns `-5557` if it fails. Preflight requires nondecreasing boundaries (`-5558`) and a positive final boundary (`-5559`). DREAM.3D v6.5.171 reads values without checking extraction success, order or the final boundary. Both implementations reject non-positive requested spacing and an input file that cannot be opened. -**None observed.** A direct A/B comparison was run against DREAM3D 6.5.171. +**Affected users:** Users whose Z-position files are incomplete, contain nonnumeric tokens among the required values, decrease between boundaries, or end at zero or a negative value. -- **Fixture:** synthetic Image Geometry, dims 3×2×4, spacing (0.5, 0.75, 1.0), origin (1, 2, 3). Cell data: `Data` (`int32`, value == voxel index), `Mask` (`DataArray`), `Vec` (`float32`, 3-component). Authored directly in legacy v7 `.dream3d` format so DREAM3D 6.5.171 and DREAM3D-NX read byte-identical input. -- **Parameters:** Z boundary positions `{0, 1, 3, 6, 10}` (ZPoints + 1 = 5 values); `new_z_spacing = 2.0`; in-place. -- **Result:** legacy 6.5.171 output, SIMPLNX output, and the independent Class-1 analytical oracle are **bit-identical** on every cell array (`Data`, `Mask`, `Vec`) and on the output geometry (dimensions 3×2×5, spacing (0.5, 0.75, 2.0), origin unchanged). The upsampled plane-repeat pattern (destination planes 2 and 3 both drawing from source plane 2) is reproduced identically by both implementations. +**Recommendation:** Trust SIMPLNX. Supply at least `ZPoints + 1` parseable boundary values in nondecreasing order, with a positive final boundary. -The port preserves the legacy Z-plane mapping rule and dimension arithmetic exactly; the port-time deltas enumerated in the report (bulk copy, new-geometry output mode, added preflight validation, parallelization, cell-AM binding) are non-behavioral for valid input, as confirmed by this comparison. +**Evidence:** Compare `ReadZBoundsFile()` in `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/RegularizeZSpacing.cpp` and `preflightImpl()` in `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/RegularizeZSpacingFilter.cpp` with `dataCheck()` and `execute()` in DREAM.3D v6.5.171 `Source/Plugins/Sampling/SamplingFilters/RegularizeZSpacing.cpp`. The `Invalid Parameters` test checks short files, decreasing boundaries and zero extent with their exact error codes. --- -## Scope note: cell-AM binding on converted pipelines (not an output deviation) +### RegularizeZSpacingFilter-D3 + +| Field | Value | +|---|---| +| **Deviation ID** | `RegularizeZSpacingFilter-D3` | +| **Filter UUID** | `d6599986-1932-4bfc-993d-71eafefe6db0` | +| **Status** | active | + +**Symptom:** SIMPLNX can keep the input geometry and write the resampled data to a separate geometry. + +**Root cause:** Algorithmic choice. `Perform In Place` defaults to true. When false, SIMPLNX creates the requested output geometry and retains the input. DREAM.3D v6.5.171 replaces the selected cell AttributeMatrix and changes the input geometry dimensions and spacing. SIMPLNX preserves the same strict `>` plane-selection rule and output Z-dimension calculation in both modes. -Legacy selected the target cell data by an explicit AttributeMatrix path; SIMPLNX selects the Image Geometry and operates on its *assigned* cell AttributeMatrix. When a legacy pipeline is converted (`FromSIMPLJson`), only the DataContainer name is kept from `CellAttributeMatrixPath` — the AM name is discarded. +**Affected users:** Users who disable `Perform In Place` and expect the input geometry to contain the resampled data. -- **Common case (one cell AM per geometry):** identical behavior; the discarded AM name and the geometry's cell AM are the same object. -- **Corner cases:** a legacy DataContainer holding more than one cell-level AM where the pipeline selected the non-canonical one will, after conversion, resample the geometry's assigned cell AM instead; a stale AM name that legacy rejected with an error converts to a pipeline that proceeds on the actual cell AM. Additionally, SIMPLNX errors on a geometry with no cell AM (`-5560`) and on non-DataArray cell-AM members (`-5561`) — states legacy either could not express or would have crashed on. +**Recommendation:** Trust SIMPLNX. Select the created geometry in downstream filters, or keep `Perform In Place` enabled for the legacy output arrangement. -These are parameter-model/conversion semantics, not output-math differences; for identical valid inputs the outputs are bit-identical (comparison above). +**Evidence:** `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/RegularizeZSpacingFilter.cpp` creates output geometry and applies deferred replacement actions only in in-place mode. `Valid Execution (New Geometry)` and `Valid Execution (In Place)` in `src/Plugins/SimplnxCore/test/RegularizeZSpacingTest.cpp` check the two output arrangements. DREAM.3D v6.5.171 `Source/Plugins/Sampling/SamplingFilters/RegularizeZSpacing.cpp::execute()` replaces the selected AttributeMatrix. From 4206ae7f8c613b337d937a35186de6e466f3c9cc Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 20:01:29 -0400 Subject: [PATCH 58/91] DOC: Rewrite the RemoveFlaggedFeatures V&V record to follow the report template Signed-off-by: Michael Jackson --- .../vv/RemoveFlaggedFeaturesFilter.md | 273 ++++++++---------- .../deviations/RemoveFlaggedFeaturesFilter.md | 160 ++++++---- .../vv/provenance/6_5_test_data_1_v2.md | 136 +++++---- 3 files changed, 301 insertions(+), 268 deletions(-) diff --git a/src/Plugins/SimplnxCore/vv/RemoveFlaggedFeaturesFilter.md b/src/Plugins/SimplnxCore/vv/RemoveFlaggedFeaturesFilter.md index 7e699c3c63..bf7c7b7c4e 100644 --- a/src/Plugins/SimplnxCore/vv/RemoveFlaggedFeaturesFilter.md +++ b/src/Plugins/SimplnxCore/vv/RemoveFlaggedFeaturesFilter.md @@ -1,206 +1,169 @@ # V&V Report: RemoveFlaggedFeaturesFilter -| | | -|-----------|------------------| -| Plugin | SimplnxCore | +| | | +|--------|--------------| +| Plugin | SimplnxCore | | SIMPLNX UUID | `6e8cc6ec-8b9b-402e-9deb-85bd1cdba743` | -| DREAM3D 6.5.171 equivalent | Two legacy filters. *Remove* maps to `RemoveFlaggedFeatures` (SIMPL UUID `a8463056-3fa7-530b-847f-7f4cb78b8602`, `Source/Plugins/Processing/ProcessingFilters/RemoveFlaggedFeatures.{h,cpp}`). *Extract* maps to `ExtractFlaggedFeatures` (SIMPL UUID `e0555de5-bdc6-5bea-ba2f-aacfbec0a022`, `Source/Plugins/Sampling/SamplingFilters/ExtractFlaggedFeatures.{h,cpp}`). | +| SIMPLNX Human Name | Remove/Extract Flagged Features | +| DREAM.3D v6.5.171 equivalent | `RemoveFlaggedFeatures` (Processing) and `ExtractFlaggedFeatures` (Sampling) | | Verified commit | ** | -| Status | READY FOR REVIEW | -| Sign-off | Michael A. Jackson , 2026-09-02 (author). Second engineer: pending second-engineer review. | +| Status | COMPLETE | +| Sign-off | Michael Jackson, 2026-09-02; second engineer: Nathan Young, 2026-09-08 | ## At a glance -| Aspect | Current state | -|------------------------|--------------------------| -| Algorithm Relationship | **Minor changes** - SIMPLNX merges legacy `RemoveFlaggedFeatures` (Processing) and `ExtractFlaggedFeatures` (Sampling) into one filter with an operation selector. The removal and fill loop is a port that now lives in the shared `FeatureRemovalUtilities`; the extract path is rebuilt on the `ComputeFeatureRect` and `CropImageGeometry` sub-filters. | -| Oracle (confirmed) | **Class 1 analytical + Class 4 invariants** - three hand-derived fixtures (5x2x1, 4x4x1, 3x3x3) pin every filled cell to the exact neighbor it was copied from, plus a Small IN100 invariant test that fills 8,535 cells. Encoded in `test/RemoveFlaggedFeaturesTest.cpp`; all 28 test cases pass in `simplnx-Rel` and `simplnx-ooc-Rel`. | -| Code paths enumerated | 25 of 34 paths are assertion-covered. The 9 uncovered paths are cancellation and defensive failure paths. | -| Tests today | 28 test cases: 8 Class 1 analytical (remove, fill, extract, extract-then-remove), 4 execute-error, 1 preflight-error, 1 preflight-warning, 1 fill-ignore-list warning, 1 empty-feature warning, 1 Class 4 Small IN100 invariant, 2 strict Feature Ids (preflight placement, shared Cell paths), 1 SIMPL 6.4/6.5 conversion, 5 kept from #1568, #1748 and #1760, and 3 original algorithm tests. The 3 original algorithm tests are kept next to their Class 1 replacements. **OOC caveat:** the `simplnx-ooc-Rel` preset registers no OOC backend, so that pass certifies compile and run in that configuration, not out-of-core data-path behavior. | -| Exemplar archive | No output archive. The inline oracle needs none. The Class 4 test reads the shared input archive `6_5_test_data_1_v2.tar.gz` (SHA512 `585b51ba...3027d6c`, sidecar exists). | -| Legacy comparison | **Run (2026-09-02)** - DREAM3D 6.5.171 and SIMPLNX were run on the 5x2x1, 4x4x1 and 3x3x3 fixtures with fill on and off, and on the 4x4x1 extract. Every removal array (25 array comparisons) matched the oracle and each other exactly. The extract geometry, origin and cell arrays matched. Seven deviations are documented. Six are bugs, all present in SIMPLNX before this branch and three (D3, D4, D6) shared with 6.5.171. D7 is an algorithmic choice. | -| Bug flags | `RemoveFlaggedFeaturesFilter-D1` through `-D6`. D1 and D2 are hangs that only SIMPLNX had; D3 and D4 are shared with 6.5.171; D5 is SIMPLNX only; D6 affects both differently. All are fixed in SIMPLNX on this branch (D1 was fixed in v7.4.2). | -| V&V phase | Discovery, oracle, reconciliation, algorithm review, tests, dual-build pass, legacy comparison, deviations and documentation are complete. Five scoped review passes (adversarial, senior engineer, CPU, memory, out-of-core) were run on the change set and their findings applied; see *Review findings*. Outstanding: second-engineer review of the oracle and this report at PR review. | +| Aspect | Current state | +|--------|---------------| +| Algorithm Relationship | Minor changes. SIMPLNX combines the two DREAM.3D v6.5.171 filters and adds validation to removal and extraction. | +| Oracle (confirmed) | Classes 1 and 4: hand-derived cell copies and feature compaction, plus Small IN100 invariants in `test/RemoveFlaggedFeaturesTest.cpp`. All pass in CI. | +| Code paths enumerated | 31 of 49 exercised. Gaps cover defensive failures, some cancellation checkpoints and inputs absent from the fixtures. | +| Tests today | 28 test cases cover all three operations, fill on/off, errors, progress, shared paths and SIMPL conversion. All pass in CI. | +| Exemplar archive | `6_5_test_data_1_v2.tar.gz` supplies Small IN100 input. Expected outputs are derived in the tests. | +| Legacy comparison | Not run for the current algorithm. The DREAM.3D v6.5.171 comparison dated 2026-09-02 predates algorithm changes; its results are in provenance. | +| Bug flags | None. | +| V&V phase | Oracle and source checks are complete. A legacy comparison with the current algorithm remains open. | ## Summary -`RemoveFlaggedFeaturesFilter` removes or extracts the **Features** marked true in a boolean feature array, optionally filling vacated **Cells** by iterative dilation from face neighbors, then compacts the feature Attribute Matrix. Verification uses three Class 1 analytical fixtures whose per-cell copy sources are hand-derived, a Class 4 Small IN100 invariant test, error and preflight tests, and SIMPL conversion checks. The removal and fill outputs are bit-identical to DREAM3D 6.5.171 on every fixture; the V&V found and fixed two SIMPLNX-only infinite loops (background cells, and distinct first-sighting neighbors as reported in issue #1698), added guards for failure modes shared with 6.5.171 (no fill progress, including the Feature Ids array listed as ignored; out-of-range FeatureId; tuple count that does not match the geometry), and fixed the extract path's swallowed sub-filter errors, its crash on a flagged feature with no cells, and the copy of its temporary bounds array into every extracted geometry. +`RemoveFlaggedFeaturesFilter` removes flagged features, extracts their bounding boxes, or does both. Class 1 fixtures check exact cell copies and feature compaction; a Class 4 test checks Small IN100 invariants. All pass in CI. ## Algorithm Relationship -*Classification:* Port | **Minor changes** | Rewrite | New filter +*Classification:* Minor changes -*Evidence:* The SIMPLNX filter was introduced in PR #474 as a merge of the two legacy filters under one operation selector (*Remove*, *Extract*, *Extract then Remove*). The `Remove` branch follows legacy `RemoveFlaggedFeatures::remove_flaggedfeatures()` and `assign_badpoints()` step for step: mark, iteratively dilate, compact. PR #1700 moved that code into `FeatureRemovalUtilities::removeFlaggedFeatures()` so `KeepRemoveRankedFeaturesFilter` can share it. The `Extract` branch replaces legacy `ExtractFlaggedFeatures::find_feature_bounds()` plus its inline `CropImageGeometry` call with the `ComputeFeatureRectFilter` and `CropImageGeometryFilter` sub-filters. - -*Port-time deltas (removal path):* - -1. Face-neighbor bookkeeping uses the shared `NeighborUtilities` helpers instead of six inline boundary tests. The traversal order (-Z, -Y, -X, +X, +Y, +Z) and the boundary rules are the same, so the output is unchanged. Confirmed by the bit-identical A/B. -2. The neighbor tally is a per-cell `discoveredFeatures`/`numHits` pair instead of the legacy feature-indexed `n[]` vector. Before PR #1700 the SIMPLNX tally did not count the first sighting, which changed output by never filling some cells (D1). With the first sighting counted, the tally is equivalent to legacy. -3. The vacated-cell test was `featureName > 0` instead of the legacy `featurename < 0`, which made background cells (FeatureId 0) permanent loop targets (D2). This branch restores `>= 0 -> skip`, matching legacy. -4. Guards with no legacy counterpart were added on this branch: FeatureId range and tuple-count validation before any modification (D4), a no-progress check that counts the cells each fill pass actually fills (D3), and the Feature Ids array is always copied even when listed in the ignore list (D3). All turn undefined behavior or an infinite loop into a deterministic error or warning and do not change valid output. -5. The per-cell neighbor buffer is allocated only when fill is enabled (PR #1700). No output effect. -6. Progress messages are throttled (PR #1340). No output effect. - -*Port-time deltas (extract path):* - -7. Bounds come from `ComputeFeatureRectFilter` (min initialized to `UINT32_MAX`, max to 0) instead of the legacy `-1` sentinel. For a flagged feature with no cells the legacy filter crops a bogus 1x1x1 geometry at origin (-1, -1, -1) (D6); SIMPLNX now warns once, listing the skipped ids, and creates nothing for them. The temporary bounds array is copied into a local buffer and deleted before the first crop, so it is neither carried into the extracted geometries nor left behind on an error return. -8. The extracted geometry is named `-` and carries the feature Attribute Matrix. Legacy names it `Feature_` and copies only cell data (D7). -9. Sub-filter preflight and execute results were tested against the wrong variable and failures threw `std::runtime_error` (D5). This branch returns error results `-53901` through `-53904`. - -*Material PRs since the filter was introduced:* #474 (introduced), #926 (NeighborList removal warning), #1017 (DataStore), #1278 (FeatureId range checks elsewhere), #1523 (shared face-neighbor helpers), #1590 (2D image handling), #1700 (shared `FeatureRemovalUtilities`, first-sighting fix). See the deviation entries for which of these changed output. +*Evidence:* The Direct and Scanline implementations preserve the legacy mark, face-neighbor fill and compaction rule; extraction delegates bounds and crops to `ComputeFeatureRectFilter` and `CropImageGeometryFilter`. ## Oracle -*Class:* **1 (Analytical)** primary + **4 (Invariant)** companion. - -Expected values are derived by hand from the fixture definitions and the documented face-neighbor rule, without using output from SIMPLNX or DREAM3D 6.5.171. - -*Applied:* Every fixture carries three cell arrays: `FeatureIds`, `CellValue = 100 + cell index`, and `IgnoredValue = 500 + cell index`, plus a feature array `Int32DataSet = 1000 + feature id`. Because `CellValue` is unique per cell, the value found in a vacated cell after the fill identifies exactly which neighbor it was copied from, and copies that chain across iterations are visible. The derivations are written as comments beside each `REQUIRE` in the test. +*Class:* 1 (Analytical), with 4 (Invariant). -| Fixture | Geometry | What it pins | -|---|---|---| -| A (issue #1698) | 5x2x1, remove features 2 and 3 | A vacated cell whose valid neighbors are all distinct features (first sighting must count), a cell with one valid neighbor, a cell with no valid neighbor in pass 1 that is filled in pass 2, a 1-vs-1 tie resolved to the first-seen feature, and a two-step copy chain (cell 4 receives cell 2's value through cell 3). Expected `FeatureIds = [1,1,1,1,1, 1,2,2,2,2]`. | -| B (background) | 4x4x1 with five FeatureId 0 cells, remove feature 3 | Background cells are never fill targets and are legal fill sources: cell 14 is filled from background cell 15 and ends at 0. Expected `FeatureIds = [0,1,1,1, 1,0,2,2, 2,2,0,1, 2,2,0,0]`. Also the extract fixture: feature 3 crops to a 2x1x1 geometry at origin (1, 3, 0). | -| E (3D) | 3x3x3, remove features 3 and 4 | All six face directions, a 4-vs-2 majority where the losing feature is seen first (cell 13 copies from +Z cell 22), and corner cell 0 enclosed by vacated cells so that it is filled only in pass 2 from cell 9, which itself was filled from cell 18. | -| Small IN100 | 100x100x100, remove 275 features with fewer than 100 cells, fill on | Class 4: every cell ends in a surviving feature, untouched cells follow the compaction renumbering exactly, feature arrays are copied not recomputed, each cell's phase equals its new feature's phase, every one of the 8,535 filled cells shares its final id with a face neighbor, and survivors grow by exactly the number of filled cells. NeighborLists are removed. | +*Applied:* Four hand-built fixtures (5×2×1, 4×4×1, 3×3×3 and a 12-feature line) define exact IDs, copy sources and crops; Small IN100 supplies compaction and conservation invariants. -*Encoded:* `src/Plugins/SimplnxCore/test/RemoveFlaggedFeaturesTest.cpp` - `Class 1 Oracle - *` (8 test cases) and `Class 4 Invariants - Small IN100 remove small features with fill`. All pass at the verified commit in the `simplnx-Rel` and `simplnx-ooc-Rel` builds. +*Encoded:* `src/Plugins/SimplnxCore/test/RemoveFlaggedFeaturesTest.cpp::Class 1 Oracle - *` (8 cases) and `Class 4 Invariants - Small IN100 remove small features with fill` (1 case). All pass in CI. -*Second-engineer review:* pending second-engineer review (the PR review constitutes the review). +*Second-engineer review:* Nathan Young, 2026-09-08. ## Bugs found and fixed -*This branch fixes every defect in this table except D1, which PR #1700 fixed in DREAM3D-NX v7.4.2 before this V&V; this branch adds its regression test. The remaining fixes will be in the DREAM3D-NX release after v7.4.2.* +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 `RemoveFlaggedFeaturesFilter-D1`. -| Deviation | Defect | Affected released versions | Resolution in this branch | -|-----------|--------|----------------------------|---------------------------| -| `RemoveFlaggedFeaturesFilter-D1` | With *Fill-in Removed Features* on, the neighbor tally only recorded a fill source on the second sighting of a feature, so a vacated cell whose valid neighbors were all distinct features, or which had a single valid neighbor, was never filled and the dilation loop never terminated (GitHub issue #1698). | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D 6.5.171 was not affected. | Fixed in v7.4.2 by PR #1700 (first sighting counts as a hit). This branch adds the `Fill from distinct first-sighting neighbors (issue #1698)` regression test that reproduces the issue's 5x2x1 case. | -| `RemoveFlaggedFeaturesFilter-D2` | With fill on, any cell with FeatureId 0 was treated as an unresolved cell but never filled, so the dilation loop never terminated on any input that contained background cells. | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D 6.5.171 was not affected. | The vacated-cell test is `featureName >= 0 -> skip`, matching 6.5.171. Pinned by `Fill treats FeatureId 0 as a source and never a target`, which hung before the fix. | -| `RemoveFlaggedFeaturesFilter-D3` | With fill on, when every cell belonged to a flagged feature and the only unflagged features owned no cells, no vacated cell had a fill source and the loop spun forever. | DREAM.3D 6.5.171; DREAM3D-NX v7.0.0 through v7.4.2. | A pass that leaves vacated cells but fills none of them returns error `-45436`. The Feature Ids array is always copied even when listed in *Attribute Arrays to Ignore* (warning `-45438`), which closed a second route to the same hang. Pinned by `Execute Error - no fill progress (-45436)` and `Fill cannot ignore the Feature Ids array (-45438 warning)`. | -| `RemoveFlaggedFeaturesFilter-D4` | A FeatureId that was negative or not less than the feature tuple count indexed the flag vector out of bounds during marking. | DREAM.3D 6.5.171; DREAM3D-NX v7.0.0 through v7.4.2. | Every FeatureId is validated before any modification; a bad value returns error `-45435` naming the cell, value and valid range, and a tuple count that differs from the geometry cell count returns `-45437`. Pinned by `Execute Error - FeatureId out of range (-45435)` and `Execute Error - Feature Ids tuple count differs from the geometry (-45437)`. | -| `RemoveFlaggedFeaturesFilter-D5` | The extract path tested the preflight result where it should have tested the execute result of its sub-filters, so an execute failure was ignored, and a preflight failure threw `std::runtime_error` instead of returning an error. | DREAM3D-NX v7.0.0 through v7.4.2. Not applicable to 6.5.171. | Sub-filter preflight and execute results are checked and returned as errors `-53901` through `-53904` with the sub-filter's message. Covered by inspection; see Code path coverage rows 6, 7, 11, 12. | -| `RemoveFlaggedFeaturesFilter-D6` | A flagged feature that owns no cell has an empty bounding box. SIMPLNX passed it to the crop, which failed preflight and threw. DREAM.3D 6.5.171 crops a 1x1x1 geometry at origin (-1, -1, -1) containing cell 0. | DREAM.3D 6.5.171 (bogus geometry); DREAM3D-NX v7.0.0 through v7.4.2 (uncaught exception). | The features are skipped and one warning `-53905` lists them; no geometry is created for them. Pinned by `Extract - flagged feature with no cells is skipped with a warning`. | - -A preflight cleanup that is not a deviation: the filter emitted its own NeighborList warning (`-11505`) and the shared helper emitted a second one (`-5558`) for the same arrays. The duplicate was removed; the test asserts exactly one warning. - -## Review findings - -Five review passes were run on the change set only (adversarial, nit-picky senior engineer, CPU performance, memory, out-of-core). Findings that changed the code: - -- Adversarial: listing the Feature Ids array in *Attribute Arrays to Ignore* with fill on still hung, because the guard tested whether a source was chosen, not whether a cell was filled. The guard now counts filled cells and the Feature Ids array is never ignored (warning `-45438`). The Feature Ids tuple count is now checked against the geometry (`-45437`). -- Memory: every extracted geometry received a copy of the internal `tempBounds` array, and the array leaked on every error return. The bounds are copied to a local buffer and the array is deleted before the first crop. The dead `CreateArrayAction` in preflight (sized to the cell count) was removed. -- CPU: the per-vacated-cell `std::vector` allocations in the neighbor tally were replaced by fixed `std::array`s; the cell-array list is built once instead of once per pass; the validation pass polls the cancel flag. The `>= 0` fix also removes a per-pass allocation storm on background cells. -- Senior engineer: `-45436` reports the number of stuck cells; the empty-feature warning is aggregated; sub-filter error messages carry the bounds and a count of further errors; Doxygen completed on the new functions; inputs of the scan are `const`. -- Out-of-core: no access pattern of an existing loop changed; the validation pass is one extra chunk-sequential read. The `simplnx-ooc-Rel` caveat above was added. +| Deviation | Defect | Affected released versions | Resolution | +|-----------|--------|----------------------------|------------| +| `RemoveFlaggedFeaturesFilter-D1` | A single valid neighbor or distinct neighbor features could leave fill running forever. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | The first sighting counts as a hit and can supply the cell. | +| `RemoveFlaggedFeaturesFilter-D2` | Background cells kept the fill loop running forever. | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was not affected. | Nonnegative cells are never fill targets; background remains a valid source. | +| `RemoveFlaggedFeaturesFilter-D3` | Fill could loop forever without a source or when FeatureIds was ignored. | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was affected. | No progress returns `-45436`; FeatureIds cannot be ignored and gives warning `-45438`. | +| `RemoveFlaggedFeaturesFilter-D4` | Invalid FeatureIds or tuple counts permitted out-of-bounds access. | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was affected. | Removal validates IDs and tuple counts before changes, with errors `-45435` and `-45437`. | +| `RemoveFlaggedFeaturesFilter-D5` | Extraction ignored delegated execution errors and threw on preflight errors. | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was not affected by this result-handling defect. | Delegated failures return `-53901` through `-53904` with the cause. | +| `RemoveFlaggedFeaturesFilter-D6` | An empty flagged feature caused an exception or a false crop. | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was affected. | Warning `-53905` lists empty features; the filter creates no geometry for them. | +| `RemoveFlaggedFeaturesFilter-D8` | The internal bounds array remained in source and extracted feature data. | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was not affected. | Bounds are copied into a local buffer; `tempBounds` is removed before crops and after bounds-execution failure. | +| `RemoveFlaggedFeaturesFilter-D9` | Preflight gave duplicate NeighborList deletion warnings. | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was not affected. | Removal gives one warning `-5558`; extraction alone gives none. | ## Code path coverage -25 of 34 paths are assertion-covered. The uncovered paths are cancellation paths and defensive failure paths of the sub-filters, the marking step and the compaction helper. +31 of 49 paths exercised. Each gap is listed below. Direct and Scanline tests use in-memory stores. -Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/RemoveFlaggedFeatures.cpp` and `src/Plugins/SimplnxCore/src/SimplnxCore/utils/FeatureRemovalUtilities.cpp`, plus preflight in `Filters/RemoveFlaggedFeaturesFilter.cpp`. +Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/RemoveFlaggedFeatures.cpp` (50 lines); `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/RemoveFlaggedFeaturesDirect.cpp` (562 lines); `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/RemoveFlaggedFeaturesScanline.cpp` (838 lines); `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/RemoveFlaggedFeaturesCommon.hpp` (120 lines); `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/RemoveFlaggedFeaturesFilter.cpp` (233 lines). -Phases: (a) preflight, (b) extract (bounding boxes, then one crop per flagged feature), (c) removal marking, (d) iterative fill, (e) compaction. +Test names below omit the `SimplnxCore::RemoveFlaggedFeaturesFilter:` or `SimplnxCore::RemoveFlaggedFeatures:` prefix. | # | Phase | Path | Test case | -|---|---|---|---| -| 1 | (a) Preflight | Parent of the flag array is not an Attribute Matrix directly under the Image Geometry -> error `-9895` | `Preflight Error - flag array parent is not an Attribute Matrix (-9895)`; `Strict Feature Ids` (flag array outside the geometry, under a plain `DataGroup`, and in the Cell Attribute Matrix) | +|---|-------|------|-----------| +| 1 | (a) Preflight | Flag array parent is not a Feature Attribute Matrix in the selected geometry → error `-9895` | `Preflight Error - flag array parent is not an Attribute Matrix (-9895)`; `Strict Feature Ids` | | 2 | (b) Extract | Temporary `tempBounds` array is created by the `ComputeFeatureRect` sub-filter and deleted by the algorithm before the first crop, so no extracted geometry carries it | `Class 1 Oracle - Extract crops the bounding box of each flagged feature` (asserts `tempBounds` is absent from the source and from the extracted geometry) | | 3 | (a) Preflight | *Remove* or *Extract then Remove* -> cell and feature groups marked modified, NeighborLists scheduled for deletion, one warning `-5558` | `Preflight Warning - NeighborLists are removed (-5558)` (`Functionality = 0`, `Functionality = 2`) | | 4 | (a) Preflight | *Extract* -> no NeighborList warning, NeighborLists kept | `Preflight Warning - NeighborLists are removed (-5558)` (`Extract alone does not remove NeighborLists`) | -| 5 | (b) Extract | Flag array is not Bool or UInt8 -> error `-53900` | *Not directly tested. The parameter type check in preflight prevents it; defensive.* | -| 6 | (b) Extract | `ComputeFeatureRect` preflight fails -> error `-53901` | *Not directly tested. Requires an internal inconsistency the filter's own preflight already rejects; defensive.* | -| 7 | (b) Extract | `ComputeFeatureRect` execute fails -> error `-53902`, `tempBounds` removed | *Not directly tested. The sub-filter fails on a FeatureId beyond the feature tuple count. Extract runs before removal, so in *Extract* and *Extract then Remove* this path is reached before path 14; defensive.* | +| 5 | Validation | Missing or unsupported flag array → error `-53900` | *Not directly tested. Parameter validation rejects unsupported arrays before execution.* | +| 6 | (b) Extract | `ComputeFeatureRect` preflight fails -> error `-53901` | *Not directly tested. No fixture forces the delegated preflight to fail.* | +| 7 | (b) Extract | `ComputeFeatureRect` execute fails -> error `-53902`, `tempBounds` removed | *Not directly tested. No Extract-only fixture supplies an out-of-range FeatureId.* | | 8 | (b) Extract | One or more flagged features own no cell -> one warning `-53905` listing them, no geometry for them | `Extract - flagged feature with no cells is skipped with a warning` (two empty features, one warning) | | 9 | (b) Extract | Flagged feature -> new geometry `-` with the bounding-box dimensions, origin, all cell arrays and the feature Attribute Matrix | `Class 1 Oracle - Extract crops the bounding box of each flagged feature`, `Class 1 Oracle - Extract zero-pads names and handles several features`, `Class 1 Oracle - Extract then Remove with fill` | | 10 | (b) Extract | Unflagged feature -> no geometry | `Class 1 Oracle - Extract zero-pads names and handles several features` | -| 11 | (b) Extract | Crop preflight fails -> error `-53903` | *Not directly tested. Reachable before this branch through path 8; now unreachable for valid bounds; defensive.* | -| 12 | (b) Extract | Crop execute fails -> error `-53904` | *Not directly tested. Defensive.* | -| 13 | (b) Extract | Cancel between crops -> return | *Not directly tested. Requires cancel-signal injection.* | +| 11 | (b) Extract | Crop preflight fails -> error `-53903` | *Not directly tested. No fixture forces crop preflight to fail.* | +| 12 | (b) Extract | Crop execute fails -> error `-53904` | *Not directly tested. Requires a delegated execution failure.* | +| 13 | (b) Extract | Cancellation at extraction checkpoints outside the crop task | *Not directly tested. The cancellation test injects cancellation inside the crop task instead.* | | 14 | (c) Marking | A FeatureId is negative or >= feature tuple count -> error `-45435`, nothing modified | `Execute Error - FeatureId out of range (-45435)` (both sections) | -| 14a | (c) Marking | Feature Ids tuple count differs from the geometry cell count -> error `-45437`, nothing modified | `Execute Error - Feature Ids tuple count differs from the geometry (-45437)` | -| 14b | (c) Marking | Fewer than two feature tuples -> error `-45433`, nothing modified | *Not directly tested. A one-tuple feature Attribute Matrix cannot hold a flagged feature; defensive.* | -| 15 | (c) Marking | Every feature flagged -> error `-45433`, nothing modified | `Execute Error - all features flagged (-45433)` (`Fill = false`, `Fill = true`) | -| 16 | (c) Marking | Fill off: flagged cell -> 0 | `Class 1 Oracle - Remove without fill` | -| 17 | (c) Marking | Fill on: flagged cell -> -1 | Every `Class 1 Oracle - Fill *` test | -| 18 | (d) Fill | Vacated cell with a single valid neighbor, or all-distinct neighbors: first sighting records the source | `Class 1 Oracle - Fill from distinct first-sighting neighbors (issue #1698)` (cells 3 and 9) | -| 19 | (d) Fill | Majority vote: the source moves only when a feature's tally strictly exceeds the previous maximum | `Class 1 Oracle - Fill majority vote and second iteration (3x3x3)` (cell 13, 4 to 2) | -| 20 | (d) Fill | 1-vs-1 tie -> first-seen feature in -Z, -Y, -X, +X, +Y, +Z order | `Class 1 Oracle - Fill from distinct first-sighting neighbors (issue #1698)` (cells 3 and 4) | -| 21 | (d) Fill | Vacated cell with no non-negative neighbor this pass -> filled in a later pass from a neighbor that was itself filled | `(issue #1698)` (cell 4 via cell 3), `(3x3x3)` (cell 0 via cell 9 via cell 18) | -| 22 | (d) Fill | Background cell (FeatureId 0) is skipped as a target and counted as a source | `Class 1 Oracle - Fill treats FeatureId 0 as a source and never a target` (cell 14 -> 0) | -| 23 | (d) Fill | Vacated cells remain and the pass filled none -> error `-45436`, cells left at -1 | `Execute Error - no fill progress (-45436)` | -| 23a | (d) Fill | Feature Ids array listed in the ignore list -> removed from the list, warning `-45438`, fill proceeds | `Fill cannot ignore the Feature Ids array (-45438 warning)` | -| 24 | (d) Fill | Every non-ignored cell array is copied from the source cell; ignored arrays are untouched | `Class 1 Oracle - Fill copies every non-ignored Cell array` and the `IgnoredValue` checks in every fill test | -| 25 | (d) Fill | Face neighbors outside the volume are excluded | 5x2x1 and 4x4x1 fixtures (Y and Z faces), 3x3x3 fixture (all six faces) | -| 26 | (d) Fill | Cancel during a pass -> return | *Not directly tested. Requires cancel-signal injection.* | -| 27 | (e) Compaction | Surviving features renumbered contiguously from 1 in input order; feature arrays compacted in place; NeighborLists deleted | Every remove test (`CheckFeatureArraysCompacted`), `Class 4 Invariants - Small IN100 remove small features with fill` | -| 28 | (e) Compaction | `RemoveInactiveObjects` fails -> error `-45434` | *Not directly tested. Requires a feature group whose arrays do not match the flag count, which the preflight tuple check rejects; defensive.* | -| 29 | (a) Preflight | Cell Feature Ids is not in the selected geometry's Cell Attribute Matrix (every operation) -> error `-9893` | `Strict Feature Ids` (`Feature Ids in a second Attribute Matrix with the same tuple shape`, for all three operations). Asserts invalid preflight, the exact code and the paths in the message. The `Feature Ids through a shared Cell Attribute Matrix` section checks that a second path to the same Cell Attribute Matrix is accepted. | -| 30 | (a) Preflight | *Remove* or *Extract then Remove* with fill on, and an Attribute Arrays to Ignore entry is not in the selected geometry's Cell Attribute Matrix -> error `-9894` | `Strict Feature Ids` (`Ignored array in a second Attribute Matrix`: `-9894` for *Remove* and *Extract then Remove*, valid for *Extract*; `Unused ignore list with fill disabled`: valid) | -| 31 | (d) Fill, (e) Compaction | Feature Ids or ignored arrays selected through a second path to the Cell Attribute Matrix -> modifications are reported on the geometry's Cell path, ignored arrays are untouched, and only the selected Feature Attribute Matrix is compacted | `Shared Cell Paths and Selected Feature Matrix` (Feature Ids selected through each path) | +| 15 | (c) Marking | Feature Ids tuple count differs from the geometry cell count -> error `-45437`, nothing modified | `Execute Error - Feature Ids tuple count differs from the geometry (-45437)` | +| 16 | (c) Marking | Fewer than two feature tuples -> error `-45433`, nothing modified | *Not directly tested. No fixture has fewer than two feature tuples.* | +| 17 | (c) Marking | Every feature flagged -> error `-45433`, nothing modified | `Execute Error - all features flagged (-45433)` (`Fill = false`, `Fill = true`) | +| 18 | (c) Marking | Fill off: flagged cell -> 0 | `Class 1 Oracle - Remove without fill` | +| 19 | (c) Marking | Fill on: flagged cell -> -1 | Every `Class 1 Oracle - Fill *` test | +| 20 | (d) Fill | Vacated cell with a single valid neighbor, or all-distinct neighbors: first sighting records the source | `Class 1 Oracle - Fill from distinct first-sighting neighbors (issue #1698)` (cells 3 and 9) | +| 21 | (d) Fill | Majority vote: the source moves only when a feature's tally strictly exceeds the previous maximum | `Class 1 Oracle - Fill majority vote and second iteration (3x3x3)` (cell 13, 4 to 2) | +| 22 | (d) Fill | 1-vs-1 tie -> first-seen feature in -Z, -Y, -X, +X, +Y, +Z order | `Class 1 Oracle - Fill from distinct first-sighting neighbors (issue #1698)` (cells 3 and 4) | +| 23 | (d) Fill | A vacated cell with no nonnegative neighbor is filled in a later iteration from a neighbor that was itself filled | `Class 1 Oracle - Fill from distinct first-sighting neighbors (issue #1698)`; `Class 1 Oracle - Fill majority vote and second iteration (3x3x3)` | +| 24 | (d) Fill | Background cell (FeatureId 0) is skipped as a target and counted as a source | `Class 1 Oracle - Fill treats FeatureId 0 as a source and never a target` (cell 14 -> 0) | +| 25 | (d) Fill | Vacated cells remain and the pass filled none -> error `-45436`, cells left at -1 | `Execute Error - no fill progress (-45436)` | +| 26 | (d) Fill | Feature Ids array listed in the ignore list -> removed from the list, warning `-45438`, fill proceeds | `Fill cannot ignore the Feature Ids array (-45438 warning)` | +| 27 | (d) Fill | Every non-ignored cell array is copied from the source cell; ignored arrays are untouched | `Class 1 Oracle - Fill copies every non-ignored Cell array` and the `IgnoredValue` checks in every fill test | +| 28 | (d) Fill | Face neighbors outside the volume are excluded | `Class 1 Oracle - Remove without fill`; `Class 1 Oracle - Fill majority vote and second iteration (3x3x3)` | +| 29 | (d) Fill | Cancel during a pass -> return | *Not directly tested. Requires cancel-signal injection.* | +| 30 | (e) Compaction | Surviving features renumbered contiguously from 1 in input order; feature arrays compacted in place; NeighborLists deleted | Every remove test (`CheckFeatureArraysCompacted`), `Class 4 Invariants - Small IN100 remove small features with fill` | +| 31 | (e) Compaction | Feature compaction fails → propagate its error | *Not directly tested. Requires a compaction failure.* | +| 32 | (a) Preflight | Cell Feature Ids is not in the selected geometry's Cell Attribute Matrix (every operation) -> error `-9893` | `Strict Feature Ids` — rejects a second Attribute Matrix; accepts another path to the same Cell Attribute Matrix. | +| 33 | (a) Preflight | *Remove* or *Extract then Remove* with fill on, and an Attribute Arrays to Ignore entry is not in the selected geometry's Cell Attribute Matrix -> error `-9894` | `Strict Feature Ids` — rejects misplaced ignored arrays only when removal and fill are enabled. | +| 34 | (d) Fill, (e) Compaction | Feature Ids or ignored arrays selected through a second path to the Cell Attribute Matrix -> modifications are reported on the geometry's Cell path, ignored arrays are untouched, and only the selected Feature Attribute Matrix is compacted | `Shared Cell Paths and Selected Feature Matrix` — both Cell paths. | +| 35 | Dispatch | Direct algorithm, including scalar, vector and Boolean copies | `fill direct and scanline parity` | +| 36 | Dispatch | Scanline algorithm, including slice transfers and final slice | `fill direct and scanline parity`; `Class 1 Oracle - Fill majority vote and second iteration (3x3x3)` | +| 37 | Extract | Progress reaches 100 before the success message | `Extraction progress completes` | +| 38 | Extract | Cancellation before delegated crop execution suppresses completion | `Extraction progress does not complete after cancellation` | +| 39 | Preflight | Stale feature-array warning only for operations that remove features | `Preflight Stale Arrays Only When Modifying` | +| 40 | Validation | Boolean feature flags | `Class 1 Oracle - Remove without fill` | +| 41 | Validation | UInt8 feature flags | *Not directly tested. The fixtures use Boolean flags.* | +| 42 | Storage | Bulk read or write failure during validation, marking, fill or renumbering | *Not directly tested. Requires a store that injects transfer failures.* | +| 43 | Extract | Bounds-buffer read failure; remove the temporary array and return the error | *Not directly tested. Requires a bounds-store read failure.* | +| 44 | Scanline fill | Invalid component shape or slice-size overflow → error `-45435` | *Not directly tested. Requires invalid array metadata.* | +| 45 | Scanline fill | Source slice outside the geometry → error `-45436` | *Not directly tested. Requires an invalid internal source mark.* | +| 46 | Scanline fill | Zero Z dimension → return without filling | *Not directly tested. All fixtures have nonzero dimensions.* | +| 47 | Removal | No flagged features → leave labels unchanged | *Not directly tested. Each successful removal fixture flags at least one feature.* | +| 48 | Extract | More than ten empty features → truncate the warning list | *Not directly tested. The empty-feature fixture has two empty features.* | +| 49 | Validation/compaction | Cancellation during validation, marking or feature compaction | *Not directly tested. Requires cancellation at those checkpoints.* | ## Test inventory | Test case | Status | Notes | |-----------|--------|-------| -| `SimplnxCore::RemoveFlaggedFeaturesFilter: Class 1 Oracle - Remove without fill` | new-for-V&V | Two sections (fixtures A and B). Asserts exact `FeatureIds`, unchanged `CellValue` and `IgnoredValue`, and the compacted `Int32DataSet` and flag arrays. 36 assertions. | -| `SimplnxCore::RemoveFlaggedFeaturesFilter: Class 1 Oracle - Fill from distinct first-sighting neighbors (issue #1698)` | new-for-V&V | The issue's 5x2x1 case. Asserts exact `FeatureIds`, the copy source of each of the three filled cells via `CellValue`, the untouched ignored array and the compaction. Hung before v7.4.2. 19 assertions. | -| `SimplnxCore::RemoveFlaggedFeaturesFilter: Class 1 Oracle - Fill copies every non-ignored Cell array` | new-for-V&V | Fixture A with an empty ignore list; asserts `IgnoredValue` follows the same copies as `CellValue`. 15 assertions. | -| `SimplnxCore::RemoveFlaggedFeaturesFilter: Class 1 Oracle - Fill treats FeatureId 0 as a source and never a target` | new-for-V&V | Fixture B with fill; asserts the five background cells stay 0, cell 13 copies from cell 12 and cell 14 copies from background cell 15. Hung before this branch (D2). 19 assertions. | -| `SimplnxCore::RemoveFlaggedFeaturesFilter: Class 1 Oracle - Fill majority vote and second iteration (3x3x3)` | new-for-V&V | Fixture E; asserts all 27 `FeatureIds`, the five copy sources including the two-pass chain into cell 0, ignored array and compaction. 19 assertions. | -| `SimplnxCore::RemoveFlaggedFeaturesFilter: Execute Error - all features flagged (-45433)` | new-for-V&V | `DYNAMIC_SECTION` for fill off and on; asserts the exact code and that no array was modified. 21 assertions. | -| `SimplnxCore::RemoveFlaggedFeaturesFilter: Execute Error - FeatureId out of range (-45435)` | new-for-V&V | Sections for a value equal to the tuple count and for a negative value; asserts the code and that `FeatureIds` is unmodified. Covers D4. 15 assertions. | -| `SimplnxCore::RemoveFlaggedFeaturesFilter: Execute Error - Feature Ids tuple count differs from the geometry (-45437)` | new-for-V&V | Geometry X dimension is increased by one so that Feature Ids no longer has one tuple per cell; asserts the code and that the array is unmodified. Covers D4. 9 assertions. | -| `SimplnxCore::RemoveFlaggedFeaturesFilter: Execute Error - no fill progress (-45436)` | new-for-V&V | 4x1x1 where the only unflagged feature owns no cell; asserts the code and that all cells are left at -1. Covers D3. 8 assertions. | -| `SimplnxCore::RemoveFlaggedFeaturesFilter: Fill cannot ignore the Feature Ids array (-45438 warning)` | new-for-V&V | Fixture A with the Feature Ids array in the ignore list; asserts exactly one warning `-45438` and the same output as the issue #1698 test. Hung before this branch. Covers D3. 18 assertions. | -| `SimplnxCore::RemoveFlaggedFeaturesFilter: Class 1 Oracle - Extract crops the bounding box of each flagged feature` | new-for-V&V | Fixture B extract; asserts the original is untouched, the new geometry's dimensions (2,1,1), origin (1,3,0), spacing, all three cell arrays, the carried feature Attribute Matrix, and that `tempBounds` is absent from both the source and the extracted geometry. 35 assertions. | -| `SimplnxCore::RemoveFlaggedFeaturesFilter: Class 1 Oracle - Extract zero-pads names and handles several features` | new-for-V&V | 12-feature 1D fixture; asserts two-digit padding (`-03`, `-11`), per-feature dimensions, origin and cell values, and that unflagged features produce no geometry. 37 assertions. | -| `SimplnxCore::RemoveFlaggedFeaturesFilter: Extract - flagged feature with no cells is skipped with a warning` | new-for-V&V | Two flagged features own no cell; asserts exactly one warning `-53905` that names both, that feature 3 is still extracted and that features 4 and 5 produce no geometry. Covers D6. 27 assertions. | -| `SimplnxCore::RemoveFlaggedFeaturesFilter: Class 1 Oracle - Extract then Remove with fill` | new-for-V&V | Fixture B; asserts the extracted geometry (from the unmodified input) and the filled original in one run. 30 assertions. | -| `SimplnxCore::RemoveFlaggedFeaturesFilter: Preflight Error - flag array parent is not an Attribute Matrix (-9895)` | new-for-V&V | Flag array placed in a plain `DataGroup`; asserts invalid preflight and the code. 5 assertions. | -| `SimplnxCore::RemoveFlaggedFeaturesFilter: Preflight Warning - NeighborLists are removed (-5558)` | new-for-V&V | `DYNAMIC_SECTION` for *Remove* and *Extract then Remove* asserts exactly one warning with code `-5558` and that the NeighborList is gone after execution; a third section asserts *Extract* emits no warning and keeps it. 23 assertions. | -| `SimplnxCore::RemoveFlaggedFeaturesFilter: Class 4 Invariants - Small IN100 remove small features with fill` | new-for-V&V | Loads `6_5_test_data_1_v2`, flags the 275 features with fewer than 100 cells, fills, and checks the six invariants listed in the Oracle section over 1,000,000 cells with aggregated counts. 751 assertions. | -| `SimplnxCore::RemoveFlaggedFeaturesFilter: Strict Feature Ids` | new-for-V&V | Inline 4x1x1 geometry with a second Cell-shaped Attribute Matrix and an external Feature Attribute Matrix. `GENERATE` over the three operations, with sections for the correct paths, `-9893`, three `-9895` placements, a shared Cell Attribute Matrix, `-9894`, and an unused ignore list with fill off. Asserts the exact code and that the message names the offending array, the Cell Attribute Matrix and the geometry. Covers paths 1, 29 and 30. | -| `SimplnxCore::RemoveFlaggedFeaturesFilter: Shared Cell Paths and Selected Feature Matrix` | new-for-V&V | Cell Attribute Matrix with a second parent, and a flag array that also lives outside the geometry. Runs *Remove* with fill, with Feature Ids selected through each path. Asserts that no modification is reported on the alias path, that the ignored array is unchanged, that only the selected Feature Attribute Matrix is compacted, and that the external array keeps its 3 tuples. Covers path 31. | -| `SimplnxCore::RemoveFlaggedFeaturesFilter: Preflight Stale Arrays Only When Modifying` | kept | Added by #1748. Asserts that preflight reports `Stale Arrays` for *Remove* and *Extract then Remove*, and not for *Extract*. | -| `SimplnxCore::RemoveFlaggedFeaturesFilter: Extraction progress completes` | kept | Added by #1760. Fixture B extract; asserts that the `Extracting Features:` progress never decreases, reaches 100 before the success message, and that the extracted Feature Ids are correct. | -| `SimplnxCore::RemoveFlaggedFeaturesFilter: Extraction progress does not complete after cancellation` | kept | Added by #1760. Sets the cancel flag on the first extraction progress message; asserts error `-1`, no extracted geometry, progress below 100 and no success message. | -| `SimplnxCore::RemoveFlaggedFeatures: fill direct and scanline parity` | kept | Added by #1568. Runs *Remove* with fill under each in-memory algorithm scenario; asserts the expected Feature Ids, copied multi-component and bool cell arrays, and compacted feature values, and that every scenario gives the same output. | -| `SimplnxCore::RemoveFlaggedFeatures: fill terminates with background zeros present` | kept | Added by #1568. Fill with background cells under a 30-second watchdog; asserts that the fill finishes, that no negative ids remain and that only the two removed cells change. | -| `SimplnxCore::RemoveFlaggedFeaturesFilter: SIMPL Backwards Compatibility` | kept | Two `DYNAMIC_SECTION`s (`SIMPL 6.5 (UUID)`, `SIMPL 6.4 (Filter_Name)`); modified this cycle to also assert the two converted ignored-array paths. 23 assertions. | -| `SimplnxCore::RemoveFlaggedFeatures: Test Remove Algorithm` | kept (legacy) | Runs *Remove* with fill off on the 4x4x1 fixture and checks `FeatureIds` and the compacted feature array. Superseded by `Class 1 Oracle - Remove without fill` (fixture B), which adds the cell-array and ignored-array checks. It does not run the fill path. | -| `SimplnxCore::RemoveFlaggedFeatures: Test Extract Algorithm` | kept (legacy) | Checks only two `FeatureIds` values in the extracted geometry. Superseded by `Class 1 Oracle - Extract crops the bounding box of each flagged feature`, which also checks dimensions, origin, spacing, every cell array and the feature Attribute Matrix. | -| `SimplnxCore::RemoveFlaggedFeatures: Test Extract then Remove Algorithm` | kept (legacy) | Fill off; same two-value extract check. Superseded by `Class 1 Oracle - Extract then Remove with fill`. | - -There are 28 active `TEST_CASE`s: 19 new for V&V, 1 kept and modified this cycle, 5 kept from other PRs (#1568, #1748, #1760), and 3 legacy algorithm tests kept next to their Class 1 replacements. `KeepRemoveRankedFeaturesTest.cpp` gained one section (`Fill terminates when the input has background cells`) because that filter shares the fill utility and inherited D2. +| `SimplnxCore::RemoveFlaggedFeaturesFilter: Class 1 Oracle - Remove without fill` | new-for-V&V | Fixtures A and B: exact FeatureIds, unchanged cell arrays and compacted feature arrays; fill disabled. | +| `SimplnxCore::RemoveFlaggedFeaturesFilter: Class 1 Oracle - Fill from distinct first-sighting neighbors (issue #1698)` | new-for-V&V | Fixture A: exact IDs and copy sources for single neighbors, distinct neighbors, ties and a second fill iteration (D1). | +| `SimplnxCore::RemoveFlaggedFeaturesFilter: Class 1 Oracle - Fill copies every non-ignored Cell array` | new-for-V&V | Fixture A with an empty ignore list: both companion arrays follow the selected copy sources. | +| `SimplnxCore::RemoveFlaggedFeaturesFilter: Fill cannot ignore the Feature Ids array (-45438 warning)` | new-for-V&V | Exactly one warning `-45438`; FeatureIds still fill when selected in the ignore list (D3). | +| `SimplnxCore::RemoveFlaggedFeaturesFilter: Class 1 Oracle - Fill treats FeatureId 0 as a source and never a target` | new-for-V&V | Fixture B: background cells remain zero and can supply a removed cell (D2). | +| `SimplnxCore::RemoveFlaggedFeaturesFilter: Class 1 Oracle - Fill majority vote and second iteration (3x3x3)` | new-for-V&V | Fixture E: all 27 IDs, six face directions, a 4-to-2 majority and a two-iteration copy chain. | +| `SimplnxCore::RemoveFlaggedFeaturesFilter: Execute Error - all features flagged (-45433)` | new-for-V&V | Fill off/on dynamic sections: error `-45433` and unchanged arrays. | +| `SimplnxCore::RemoveFlaggedFeaturesFilter: Execute Error - FeatureId out of range (-45435)` | new-for-V&V | Negative and upper-bound IDs: error `-45435` before modification (D4). | +| `SimplnxCore::RemoveFlaggedFeaturesFilter: Execute Error - Feature Ids tuple count differs from the geometry (-45437)` | new-for-V&V | Geometry and FeatureIds tuple counts differ: error `-45437` before modification (D4). | +| `SimplnxCore::RemoveFlaggedFeaturesFilter: Execute Error - no fill progress (-45436)` | new-for-V&V | No unflagged feature owns cells: error `-45436`; removed IDs remain -1 (D3). | +| `SimplnxCore::RemoveFlaggedFeaturesFilter: Extraction progress completes` | kept | Extraction progress never decreases and reaches 100 before success; extracted IDs equal `[3,3]`. | +| `SimplnxCore::RemoveFlaggedFeaturesFilter: Extraction progress does not complete after cancellation` | kept | Cancellation at the first extraction progress message: error `-1`, no geometry and no completion message. | +| `SimplnxCore::RemoveFlaggedFeaturesFilter: Class 1 Oracle - Extract crops the bounding box of each flagged feature` | new-for-V&V | Fixture B: dimensions, origin, spacing, all three cell arrays and copied feature arrays; no `tempBounds` (D8). | +| `SimplnxCore::RemoveFlaggedFeaturesFilter: Class 1 Oracle - Extract zero-pads names and handles several features` | new-for-V&V | Features 3 and 11: two-digit names, exact crops and cell values; no geometry for unflagged features. | +| `SimplnxCore::RemoveFlaggedFeaturesFilter: Extract - flagged feature with no cells is skipped with a warning` | new-for-V&V | Two empty features: one warning `-53905` naming both; only the populated feature is extracted (D6). | +| `SimplnxCore::RemoveFlaggedFeaturesFilter: Class 1 Oracle - Extract then Remove with fill` | new-for-V&V | Fixture B: crop uses the original values; removal then fills and compacts the original geometry. | +| `SimplnxCore::RemoveFlaggedFeaturesFilter: Preflight Stale Arrays Only When Modifying` | kept | Stale feature-array notice appears for Remove and Extract then Remove; absent for Extract. | +| `SimplnxCore::RemoveFlaggedFeaturesFilter: Preflight Error - flag array parent is not an Attribute Matrix (-9895)` | new-for-V&V | Flag array under a plain DataGroup: preflight error `-9895`. | +| `SimplnxCore::RemoveFlaggedFeaturesFilter: Preflight Warning - NeighborLists are removed (-5558)` | new-for-V&V | Remove and Extract then Remove dynamic sections: exactly one `-5558` warning and NeighborList deletion; Extract preserves it (D9). | +| `SimplnxCore::RemoveFlaggedFeaturesFilter: Class 4 Invariants - Small IN100 remove small features with fill` | new-for-V&V | Small IN100: flags 275 features and checks compaction, valid IDs, phase agreement, NeighborList removal and conservation of 8,535 filled cells. | +| `SimplnxCore::RemoveFlaggedFeatures: Test Remove Algorithm` | kept | Fill disabled: exact 4×4×1 FeatureIds and compacted feature values. | +| `SimplnxCore::RemoveFlaggedFeatures: Test Extract Algorithm` | kept | Extracted IDs `[3,3]`; original IDs and original feature tuples remain unchanged. | +| `SimplnxCore::RemoveFlaggedFeatures: fill direct and scanline parity` | kept | Direct and Scanline scenarios: exact IDs, two-component int32 copies, Boolean copies and compacted feature values. | +| `SimplnxCore::RemoveFlaggedFeatures: fill terminates with background zeros present` | kept | Watchdog guards termination; no negative IDs remain and only the two removed cells change. | +| `SimplnxCore::RemoveFlaggedFeatures: Test Extract then Remove Algorithm` | kept | Fill disabled: exact extracted IDs and removal/compaction in the original geometry. | +| `SimplnxCore::RemoveFlaggedFeaturesFilter: Strict Feature Ids` | new-for-V&V | All three operations: Cell and Feature Attribute Matrix placement, shared paths, ignored-array validation and fill-disabled acceptance. | +| `SimplnxCore::RemoveFlaggedFeaturesFilter: Shared Cell Paths and Selected Feature Matrix` | new-for-V&V | Both Cell paths: modifications use the geometry path, ignored values stay unchanged and only the selected Feature Attribute Matrix is compacted. | +| `SimplnxCore::RemoveFlaggedFeaturesFilter: SIMPL Backwards Compatibility` | kept | SIMPL 6.5 (UUID) and SIMPL 6.4 (Filter_Name) dynamic sections: converted values, paths and both ignored-array paths. | ## Exemplar archive -No output archive was created. Both Class 1 fixtures and their expected values are encoded inline. - -The Class 4 test uses this shared input archive: - -| Field | Value | -|---|---| -| Archive | `6_5_test_data_1_v2.tar.gz` | -| SHA512 | `585b51ba1da9784a204fe88073ca562b45afd7007cf451b0193079b885c4b4caff7cf21b13e016433b84155546ac0f73f003a8b8ebb1c58360b2c56de3027d6c` | -| CMake registration | `src/Plugins/SimplnxCore/test/CMakeLists.txt` | -| Provenance | `src/Plugins/SimplnxCore/vv/provenance/6_5_test_data_1_v2.md` | -| Role in this V&V | Input only. The test derives the flag array, the expected compaction and every invariant from the pre-filter arrays. No archived array is compared against filter output. | - -## Deviations from DREAM3D 6.5.171 +- **Archive:** `6_5_test_data_1_v2.tar.gz` +- **SHA512:** `585b51ba1da9784a204fe88073ca562b45afd7007cf451b0193079b885c4b4caff7cf21b13e016433b84155546ac0f73f003a8b8ebb1c58360b2c56de3027d6c` +- **Provenance:** `src/Plugins/SimplnxCore/vv/provenance/6_5_test_data_1_v2.md` -On 2026-09-02, DREAM3D 6.5.171 and SIMPLNX were run on legacy-native copies of fixtures A, B and E with fill on and off (five removal cases) and on fixture B with *Extract*. The inputs were authored with the shared h5py legacy writer so both runners read identical files. All 25 removal array comparisons (`FeatureIds`, `CellValue`, `IgnoredValue`, `Int32DataSet`, `Active`) matched the analytical oracle and each other exactly. The extracted geometry's dimensions, origin and three cell arrays matched. Three further fixtures were run to characterize the error-path deviations: the no-progress twin (D3), an out-of-range FeatureId (D4) and a flagged feature with no cells (D6). The pipelines, inputs, outputs, logs and comparison script are archived outside the repository in the V&V OneDrive library, folder `RemoveFlaggedFeaturesFilter` (59 files, uploaded 2026-09-03), per the archival policy. +## Deviations from DREAM.3D v6.5.171 -See `vv/deviations/RemoveFlaggedFeaturesFilter.md` for the root cause, affected users and recommendation for each deviation. +See `vv/deviations/RemoveFlaggedFeaturesFilter.md` for the root cause, affected users and recommendation. | Deviation | Observed difference | |-----------|---------------------| -| `RemoveFlaggedFeaturesFilter-D1` | SIMPLNX v7.0.0 through v7.4.1 never returned when a vacated cell's valid neighbors were all distinct features or a single neighbor; 6.5.171 completed. Fixed in v7.4.2. | -| `RemoveFlaggedFeaturesFilter-D2` | SIMPLNX through v7.4.2 never returned when the input contained FeatureId 0 cells and fill was on; 6.5.171 completed. Fixed on this branch. | -| `RemoveFlaggedFeaturesFilter-D3` | SIMPLNX returns `-45436` when a fill pass fills no cell, and always copies the Feature Ids array even when it is listed as ignored (warning `-45438`); 6.5.171 loops forever in both situations. | -| `RemoveFlaggedFeaturesFilter-D4` | SIMPLNX returns `-45435` for a FeatureId outside `[0, tuple count)` and `-45437` for a tuple count that differs from the geometry; 6.5.171 reads out of bounds and silently zeroed the cell in the A/B run. | -| `RemoveFlaggedFeaturesFilter-D5` | SIMPLNX returns `-53901` through `-53904` when an extract sub-filter fails; earlier SIMPLNX ignored execute failures and threw on preflight failures. Not applicable to 6.5.171. | -| `RemoveFlaggedFeaturesFilter-D6` | For a flagged feature with no cells SIMPLNX warns `-53905` and creates nothing; 6.5.171 creates a 1x1x1 geometry at origin (-1, -1, -1) holding cell 0. | -| `RemoveFlaggedFeaturesFilter-D7` | Extracted geometries are named `-` and carry the feature Attribute Matrix; 6.5.171 names them `Feature_` and copies only cell data. | - -Behavior that is identical in both implementations and is recorded here because it surprises users: background cells (FeatureId 0) are legal fill sources. A vacated cell whose non-negative neighbors are all background becomes background. This is the documented isotropic-coarsening rule shared with `RequireMinimumSizeFeatures` and `RequireMinNumNeighbors`. +| `RemoveFlaggedFeaturesFilter-D3` | SIMPLNX stops when fill makes no progress and prevents FeatureIds from being ignored; DREAM.3D can loop forever. | +| `RemoveFlaggedFeaturesFilter-D4` | SIMPLNX rejects invalid IDs and tuple counts before removal; DREAM.3D can access memory out of bounds. | +| `RemoveFlaggedFeaturesFilter-D6` | SIMPLNX skips empty flagged features with a warning; DREAM.3D can produce a false one-cell crop. | +| `RemoveFlaggedFeaturesFilter-D7` | SIMPLNX uses configurable names with padded IDs and copies the Feature Attribute Matrix; DREAM.3D uses `Feature_` and copies only cell data. | diff --git a/src/Plugins/SimplnxCore/vv/deviations/RemoveFlaggedFeaturesFilter.md b/src/Plugins/SimplnxCore/vv/deviations/RemoveFlaggedFeaturesFilter.md index 34f1aeb253..9734cd56aa 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/RemoveFlaggedFeaturesFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/RemoveFlaggedFeaturesFilter.md @@ -1,133 +1,183 @@ -# Deviations from DREAM3D 6.5.171: RemoveFlaggedFeaturesFilter +# Deviations from DREAM.3D v6.5.171: RemoveFlaggedFeaturesFilter -This file lists every documented behavioral difference between SIMPLNX `RemoveFlaggedFeaturesFilter` and its DREAM3D 6.5.171 equivalents, `RemoveFlaggedFeatures` (*Remove*) and `ExtractFlaggedFeatures` (*Extract*). - -Entries are referenced by stable ID (`RemoveFlaggedFeaturesFilter-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. - -On valid input (every FeatureId in range, at least one unflagged feature that owns cells, every flagged feature owning at least one cell) the removal and fill outputs of the two implementations are bit-identical on every fixture run, and the extracted geometry's dimensions, origin and cell arrays match. The deviations below concern inputs outside that envelope, two SIMPLNX-only defects that are fixed, and the packaging of extracted geometries. +Part 1 lists current differences from DREAM.3D v6.5.171. Part 2 lists fixed SIMPLNX defects. All entries share one stable ID sequence. --- -## RemoveFlaggedFeaturesFilter-D1 +## Part 1 — Differences from DREAM.3D v6.5.171 + +### RemoveFlaggedFeaturesFilter-D3 | Field | Value | |---|---| -| **Deviation ID** | `RemoveFlaggedFeaturesFilter-D1` | +| **Deviation ID** | `RemoveFlaggedFeaturesFilter-D3` | | **Filter UUID** | `6e8cc6ec-8b9b-402e-9deb-85bd1cdba743` | -| **Status** | retired 2026-08-13 (fixed in DREAM3D-NX v7.4.2 by PR #1700); regression test added 2026-09-02 | +| **Status** | active | -**Symptom:** With *Fill-in Removed Features* enabled, DREAM3D-NX v7.0.0 through v7.4.1 never returned when a vacated cell's valid face neighbors all belonged to distinct features, or when it had a single valid neighbor. DREAM3D 6.5.171 completed and filled the cell. Reported as GitHub issue #1698. +**Symptom:** SIMPLNX returns `-45436` when no removed cell can be filled; DREAM.3D loops forever. SIMPLNX also prevents FeatureIds from being ignored and emits warning `-45438`. -**Root cause:** `bug` in SIMPLNX. The per-cell tally in `IdentifyNeighbors()` pushed a newly seen feature onto `discoveredFeatures` without incrementing its hit count, so a fill source was only recorded on the second sighting of the same feature. A cell with no repeated neighbor feature never received a source, `FindVoxelArrays()` copied nothing into it, it stayed at -1, and the `do { } while(shouldLoop)` loop repeated the same pass forever. The legacy `n[feature]++` tally counted the first sighting. On large 3D volumes most vacated cells have two face neighbors in the same feature, which hid the defect; it surfaces on thin volumes and on small or isolated removed regions. +**Root cause:** bug. An unflagged feature with no cells passes the all-flagged check but cannot supply fill values. Neither earlier implementation detected a pass without progress. Ignoring FeatureIds also prevented progress. The current algorithm checks progress and always copies FeatureIds. -**Affected users:** Anyone who enabled *Fill-in Removed Features* on DREAM3D-NX v7.0.0 through v7.4.1 with thin geometries (one slice or one voxel deep) or with small isolated removed features. The pipeline had to be killed; no output was written. +**Affected users:** Users who flag every occupied feature or include FeatureIds among the ignored arrays with fill enabled. DREAM3D-NX v7.0.0 through v7.4.2 and DREAM.3D v6.5.171 were affected. -**Recommendation:** `trust SIMPLNX` v7.4.2 or later, whose output is identical to 6.5.171 on the issue's 5x2x1 case and on every other fixture in this V&V. Users on v7.4.1 or earlier must upgrade or disable fill. +**Recommendation:** Trust SIMPLNX. The fix will be in the DREAM3D-NX release after v7.4.2. Keep an unflagged feature that owns cells, or disable fill; after `-45436`, removed cells remain -1. ---- +**Evidence:** The tagged legacy removal source and every release source through v7.4.2 omit the progress guard. `Execute Error - no fill progress (-45436)` and `Fill cannot ignore the Feature Ids array (-45438 warning)` cover the fixes. `vv_work:RemoveFlaggedFeaturesFilter/ReadMe.md` and `vv_work:RemoveFlaggedFeaturesFilter/log_6_5_171_C_hang.txt` record the legacy timeout. -## RemoveFlaggedFeaturesFilter-D2 +### RemoveFlaggedFeaturesFilter-D4 | Field | Value | |---|---| -| **Deviation ID** | `RemoveFlaggedFeaturesFilter-D2` | +| **Deviation ID** | `RemoveFlaggedFeaturesFilter-D4` | | **Filter UUID** | `6e8cc6ec-8b9b-402e-9deb-85bd1cdba743` | -| **Status** | retired 2026-09-02 (fixed on the V&V branch) | +| **Status** | active | -**Symptom:** With *Fill-in Removed Features* enabled, DREAM3D-NX v7.0.0 through v7.4.2 never returned when the Feature Ids array contained any cell with FeatureId 0. DREAM3D 6.5.171 completed and left the background cells at 0. +**Symptom:** SIMPLNX rejects invalid FeatureIds or tuple counts before removal; DREAM.3D can read beyond an array. -**Root cause:** `bug` in SIMPLNX. `IdentifyNeighbors()` skipped cells with `featureName > 0`, so background cells (0) were treated as unresolved and set `shouldLoop`, but `FindVoxelArrays()` only copies into cells with `featureName < 0`. Background cells were therefore never overwritten and every pass ended with `shouldLoop == true`. The legacy loop tests `featurename < 0`. SIMPLNX now tests `featureName >= 0 -> skip`, matching 6.5.171: background is never a fill target but remains a legal fill source (see the note at the end of the V&V report). +**Root cause:** bug. Earlier removal code indexed the active-feature vector without checking each ID. It also accepted a FeatureIds tuple count that differed from the geometry cell count. Remove and Extract then Remove now validate both before modification, with errors `-45435` and `-45437`. -**Affected users:** Anyone who enabled fill on data with unassigned (background) cells, which is the common case after a segmentation with a mask. The pipeline had to be killed. +**Affected users:** Users whose FeatureIds do not match the selected feature data or geometry. DREAM3D-NX v7.0.0 through v7.4.2 and DREAM.3D v6.5.171 were affected. -**Recommendation:** `trust SIMPLNX` at or after this fix; its output is identical to 6.5.171 on the 4x4x1 background fixture. Users on v7.4.2 or earlier must disable fill on data with background cells. +**Recommendation:** Trust SIMPLNX. The fix will be in the DREAM3D-NX release after v7.4.2. Correct the selected arrays or IDs before removal; validation errors leave the data unchanged. ---- +**Evidence:** The tagged legacy source and every release source through v7.4.2 contain unchecked marking. The two execute-error tests assert the error codes and unchanged IDs. `vv_work:RemoveFlaggedFeaturesFilter/ReadMe.md` records the legacy out-of-range result on fixture D. -## RemoveFlaggedFeaturesFilter-D3 +### RemoveFlaggedFeaturesFilter-D6 | Field | Value | |---|---| -| **Deviation ID** | `RemoveFlaggedFeaturesFilter-D3` | +| **Deviation ID** | `RemoveFlaggedFeaturesFilter-D6` | | **Filter UUID** | `6e8cc6ec-8b9b-402e-9deb-85bd1cdba743` | | **Status** | active | -**Symptom:** With fill enabled, when every cell belongs to a flagged feature and the unflagged features own no cells, SIMPLNX returns error `-45436` and leaves the cells at -1; DREAM3D 6.5.171 loops forever. Reproduced on a 4x1x1 volume with `FeatureIds = [1, 1, 1, 1]`, three feature tuples, and only feature 1 flagged: 6.5.171 was still running when killed after 30 seconds. +**Symptom:** SIMPLNX skips an empty flagged feature with warning `-53905`; DREAM.3D can create a false one-cell geometry at origin (-1,-1,-1). Earlier DREAM3D-NX releases threw an exception. -**Root cause:** `bug` in DREAM3D 6.5.171 (and in SIMPLNX before this branch). The all-flagged guard only inspects the flag array, so an unflagged feature that owns no cells satisfies it. After marking, no cell is non-negative, so no vacated cell ever finds a fill source and neither implementation detected the lack of progress. SIMPLNX now counts the cells that each pass actually fills and stops with a deterministic error when vacated cells remain and the count is zero. A second route to the same non-termination, listing the Feature Ids array itself in *Attribute Arrays to Ignore* so that sources were chosen but the Feature Ids array was never overwritten, is closed by always copying that array and warning (`-45438`). 6.5.171 also loops forever in that case (`voxelArrayNames.removeAll` honors the request). This is the same class of defect as `RequireMinNumNeighborsFilter-D3`. +**Root cause:** bug. DREAM.3D initialized empty bounds to -1. Earlier SIMPLNX passed minimum bounds greater than maximum bounds to the crop. The current algorithm detects empty bounds, skips those features and lists them in one warning. -**Affected users:** Users whose feature Attribute Matrix contains features that own no cells, when the flag array removes every feature that does own cells; and users who list the Feature Ids array in *Attribute Arrays to Ignore* with fill enabled. Other inputs are unaffected. +**Affected users:** Users who flag feature tuples that own no cells. DREAM3D-NX v7.0.0 through v7.4.2 and DREAM.3D v6.5.171 were affected. -**Recommendation:** `trust SIMPLNX`. The error names the array and explains the two ways to correct the input; the legacy behavior is an unbounded execution. +**Recommendation:** Trust SIMPLNX. The fix will be in the DREAM3D-NX release after v7.4.2. No geometry should represent a feature that has no cells. ---- +**Evidence:** `vv_work:RemoveFlaggedFeaturesFilter/ReadMe.md` records fixture F: DREAM.3D created `Feature_4` with ID 0 and CellValue 100. Tagged legacy extraction initializes bounds to -1. All DREAM3D-NX release sources through v7.4.2 omit the skip check. The empty-feature test asserts one warning and absent geometries. -## RemoveFlaggedFeaturesFilter-D4 +### RemoveFlaggedFeaturesFilter-D7 | Field | Value | |---|---| -| **Deviation ID** | `RemoveFlaggedFeaturesFilter-D4` | +| **Deviation ID** | `RemoveFlaggedFeaturesFilter-D7` | | **Filter UUID** | `6e8cc6ec-8b9b-402e-9deb-85bd1cdba743` | | **Status** | active | -**Symptom:** When a FeatureId is negative or not less than the feature tuple count, SIMPLNX returns error `-45435` before it modifies anything; DREAM3D 6.5.171 indexed its flag vector out of bounds. In the A/B run (5x2x1, cell 6 set to 5 with five feature tuples) 6.5.171 completed and silently zeroed that cell. The result depended on whatever memory followed the vector. +**Symptom:** SIMPLNX names extracted geometries `-` and copies the source Feature Attribute Matrix. DREAM.3D uses `Feature_` and copies only cell data. + +**Root cause:** algorithmic choice. SIMPLNX exposes a prefix and pads IDs to the width of the feature tuple count. It delegates extraction to CropImageGeometry with feature renumbering disabled, which copies feature attributes without recomputing them. -**Root cause:** `bug` in DREAM3D 6.5.171 (and in SIMPLNX before this branch). `remove_flaggedfeatures()` evaluates `activeObjects[gnum]` for every cell without validating `gnum`. SIMPLNX now validates every FeatureId against the flag count in a read-only pass before the marking pass, and also returns `-45437` when the Feature Ids tuple count differs from the geometry cell count, which neither implementation checked before. This is the same class of defect as `RequireMinNumNeighborsFilter-D2`. +**Affected users:** Users whose scripts use legacy geometry names or expect feature attributes to describe only the crop. -**Affected users:** Users with a Feature Ids array that does not correspond to the selected feature Attribute Matrix, for example after selecting the wrong Attribute Matrix or after a partial renumbering. Valid inputs are unaffected. +**Recommendation:** Trust SIMPLNX. Update geometry paths for the mandatory hyphen and padded ID. Recompute feature attributes when they must describe the cropped cells. -**Recommendation:** `trust SIMPLNX`. The error names the cell, the value and the valid range, and no data is modified. +**Evidence:** The Class 1 extraction tests assert names, crop bounds, cell values and unchanged copied feature arrays. The tagged legacy source builds `Feature_` names. `vv_work:RemoveFlaggedFeaturesFilter/ReadMe.md` records the packaging difference for fixture B. --- -## RemoveFlaggedFeaturesFilter-D5 +## Part 2 — SIMPLNX defects and behavior changes + +### RemoveFlaggedFeaturesFilter-D1 + +| Field | Value | +|---|---| +| **Deviation ID** | `RemoveFlaggedFeaturesFilter-D1` | +| **Filter UUID** | `6e8cc6ec-8b9b-402e-9deb-85bd1cdba743` | +| **Status** | retired 2026-08-13 | +| **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:** Fill could run forever when a removed cell had one valid face neighbor or when all valid neighbors had different feature IDs. + +**Root cause:** bug. The tally recorded a source only after a feature appeared twice. The first sighting now counts as one hit and can supply the cell. DREAM.3D counted the first sighting. + +**Affected users:** Users who enable fill on thin geometries or isolated removed regions. + +**Recommendation:** Trust SIMPLNX in DREAM3D-NX v7.4.2 or later. Earlier releases require fill to be disabled for these inputs. + +**Evidence:** Release sources through v7.4.1 omit the initial hit; v7.4.2 sets it in `FeatureRemovalUtilities.cpp`. The Class 1 distinct-neighbor fixture checks the exact IDs and copy sources. PR #1700 fixed the defect. + +### RemoveFlaggedFeaturesFilter-D2 + +| Field | Value | +|---|---| +| **Deviation ID** | `RemoveFlaggedFeaturesFilter-D2` | +| **Filter UUID** | `6e8cc6ec-8b9b-402e-9deb-85bd1cdba743` | +| **Status** | retired 2026-09-02 | +| **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:** Fill could run forever when input cells had FeatureId 0. + +**Root cause:** bug. The neighbor search treated background cells as unresolved, but tuple copying only changed negative IDs. The search now skips all nonnegative IDs. Background remains a valid fill source. + +**Affected users:** Users who enable fill on data with background cells. + +**Recommendation:** Trust SIMPLNX with the fix. In DREAM3D-NX v7.4.2 or earlier, disable fill when background cells are present. + +**Evidence:** Every release source through v7.4.2 uses `featureName > 0` to skip targets. The current Direct and Scanline sources use `>= 0`. The background Class 1 fixture and termination test cover this defect. + +### RemoveFlaggedFeaturesFilter-D5 | Field | Value | |---|---| | **Deviation ID** | `RemoveFlaggedFeaturesFilter-D5` | | **Filter UUID** | `6e8cc6ec-8b9b-402e-9deb-85bd1cdba743` | -| **Status** | retired 2026-09-02 (fixed on the V&V branch) | +| **Status** | retired 2026-09-02 | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was not affected by this result-handling defect. | +| **Fixed in** | The DREAM3D-NX release after v7.4.2 | -**Symptom:** In *Extract* and *Extract then Remove*, DREAM3D-NX v7.0.0 through v7.4.2 ignored a failure of the bounding-box or crop sub-filter's execute step and continued with unset bounds, and reported a sub-filter preflight failure by throwing `std::runtime_error` rather than returning a filter error. Not applicable to 6.5.171, whose extract filter calls `CropImageGeometry` directly and checks no result. +**Symptom:** Extraction ignored delegated execution failures and threw an exception for delegated preflight failures. -**Root cause:** `bug` in SIMPLNX. After each `filter.execute()` the code tested `preflightResult.outputActions.invalid()` instead of `executeResult.result.invalid()`. SIMPLNX now returns errors `-53901`/`-53902` (bounding box preflight/execute) and `-53903`/`-53904` (crop preflight/execute) carrying the sub-filter's own message. +**Root cause:** bug. After execute, SIMPLNX tested the earlier preflight result instead of the execution result. The filter now checks each result and returns errors `-53901` through `-53904` with the delegated message. DREAM.3D had no corresponding Result-based check. -**Affected users:** Users whose extract run hit a sub-filter failure, chiefly the empty-feature case in D6. Successful extractions were unaffected. +**Affected users:** Users whose bounds computation or crop fails during extraction. -**Recommendation:** `trust SIMPLNX`. Failures are now reported as ordinary filter errors with the cause attached. +**Recommendation:** Trust SIMPLNX. Use the returned error message to correct the input. ---- +**Evidence:** Every release source through v7.4.2 repeats `preflightResult.outputActions.invalid()` after execute and throws on preflight failure. The current Direct, Scanline and common crop sources return errors. These delegated failure paths have no direct fixture. -## RemoveFlaggedFeaturesFilter-D6 +### RemoveFlaggedFeaturesFilter-D8 | Field | Value | |---|---| -| **Deviation ID** | `RemoveFlaggedFeaturesFilter-D6` | +| **Deviation ID** | `RemoveFlaggedFeaturesFilter-D8` | | **Filter UUID** | `6e8cc6ec-8b9b-402e-9deb-85bd1cdba743` | | **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:** When a flagged feature owns no cell, SIMPLNX emits warning `-53905` and creates no geometry for it; DREAM3D 6.5.171 creates a 1x1x1 geometry named `Feature_` at origin (-1, -1, -1) that contains cell 0 of the source volume. Reproduced on the 4x4x1 fixture with an unused fifth feature flagged: 6.5.171 produced `Feature_4` with `FeatureIds = [0]` and `CellValue = [100]`. DREAM3D-NX v7.0.0 through v7.4.2 threw an uncaught exception on the same input (see D5). +**Symptom:** Extraction left the internal `tempBounds` array in the source and copied it into extracted feature data. -**Root cause:** `bug` in DREAM3D 6.5.171 (bogus output) and in SIMPLNX before this branch (uncaught exception). Legacy `find_feature_bounds()` initializes every bound to -1 and only updates bounds for features that own cells, so an empty feature keeps (-1, -1, -1) to (-1, -1, -1), which `CropImageGeometry` accepts as a one-voxel crop. In SIMPLNX `ComputeFeatureRect` leaves minimum > maximum for an empty feature; the crop preflight rejected that and the algorithm threw. SIMPLNX now detects minimum > maximum, warns, and skips the feature. +**Root cause:** bug. The bounds sub-filter created an array in the Feature Attribute Matrix, and extraction left it there during cropping. The current algorithm copies bounds into a local buffer and removes the array before cropping. It also removes the array when bounds execution fails. DREAM.3D kept its bounds in a private array. -**Affected users:** Users extracting from a feature Attribute Matrix with unused tuples, for example after a filter that removed features without compacting, or with a flag array that marks unused ids. The legacy output contains a geometry that does not correspond to any feature. +**Affected users:** Users who extract features and inspect, save or process the resulting feature arrays. -**Recommendation:** `trust SIMPLNX`. The warning names the feature and the array; no spurious geometry is written. +**Recommendation:** Trust SIMPLNX. Extracted feature data no longer contains the internal bounds array. ---- +**Evidence:** All release versions through v7.4.2 read `TempBoundsPath` without removing it. PR #1736 adds the cleanup. `Class 1 Oracle - Extract crops the bounding box of each flagged feature` asserts that `tempBounds` is absent from source and output. -## RemoveFlaggedFeaturesFilter-D7 +### RemoveFlaggedFeaturesFilter-D9 | Field | Value | |---|---| -| **Deviation ID** | `RemoveFlaggedFeaturesFilter-D7` | +| **Deviation ID** | `RemoveFlaggedFeaturesFilter-D9` | | **Filter UUID** | `6e8cc6ec-8b9b-402e-9deb-85bd1cdba743` | | **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:** Preflight reported the same NeighborList deletion through two warnings. -**Symptom:** An extracted geometry is named `-` with the id zero-padded to the width of the feature count (for example `Extracted_Feature-03` when there are 12 feature tuples) and carries a copy of the source feature Attribute Matrix; DREAM3D 6.5.171 names it `Feature_` with no padding and copies only the cell Attribute Matrix. The geometry's dimensions, origin, spacing and cell arrays are identical in both. +**Root cause:** bug. The filter emitted `-11505` and then invoked the shared helper, which emitted `-5558`. The current preflight uses only the helper for operations that remove features. DREAM.3D did not use this pair of warnings. -**Root cause:** `algorithmic choice`. SIMPLNX exposes the prefix as a parameter and pads so that geometries sort in id order; it reuses `CropImageGeometryFilter` with feature renumbering off, which copies the feature Attribute Matrix so feature-level attributes stay available in the extracted geometry. +**Affected users:** Users who remove features from a Feature Attribute Matrix with NeighborLists. -**Affected users:** Pipelines or scripts that reference extracted geometries by the legacy `Feature_` name, and users who expect the extracted geometry to contain only cell data. +**Recommendation:** Trust SIMPLNX. One warning identifies the NeighborLists that removal will delete; extraction alone keeps them. -**Recommendation:** `trust SIMPLNX`. Set the prefix to `Feature_` for a closer match; note that the id is still zero-padded and the feature Attribute Matrix is still carried. +**Evidence:** Every release filter source through v7.4.2 contains both the local warning and `NeighborListRemovalPreflightCode`. PR #1736 removes the local warning. `Preflight Warning - NeighborLists are removed (-5558)` checks one warning for both removal modes and none for extraction. diff --git a/src/Plugins/SimplnxCore/vv/provenance/6_5_test_data_1_v2.md b/src/Plugins/SimplnxCore/vv/provenance/6_5_test_data_1_v2.md index 8d1105b394..cb3dc2ef68 100644 --- a/src/Plugins/SimplnxCore/vv/provenance/6_5_test_data_1_v2.md +++ b/src/Plugins/SimplnxCore/vv/provenance/6_5_test_data_1_v2.md @@ -1,89 +1,109 @@ # Exemplar Archive Provenance: 6_5_test_data_1_v2.tar.gz -This sidecar records the known provenance and test roles of the shared `6_5_test_data_1_v2.tar.gz` archive. The archive predates the current V&V policy and is used as both an input fixture and a legacy regression reference by several SimplnxCore tests. +This shared archive supplies Small IN100 input and crop regression data. This sidecar also records the independent oracle and legacy comparison for `RemoveFlaggedFeaturesFilter`. -## Archive Identity +## Archive identity | Field | Value | |---|---| | **Archive** | `6_5_test_data_1_v2.tar.gz` | | **SHA512** | `585b51ba1da9784a204fe88073ca562b45afd7007cf451b0193079b885c4b4caff7cf21b13e016433b84155546ac0f73f003a8b8ebb1c58360b2c56de3027d6c` | -| **Archive member** | `6_5_test_data_1_v2/6_5_test_data_1_v2.dream3d` | -| **Used by tests** | 20 active test cases across six SimplnxCore test source files | -| **Generated by** | Unknown. The archive was created before archive-level provenance was recorded. | -| **Generated on** | Unknown. The first repository reference was committed on 2024-07-18. | -| **First repository reference** | PR [#1021](https://github.com/BlueQuartzSoftware/simplnx/pull/1021), commit `c3be139e2ca0bbbb151d239834e8364ad70aaad1` | -| **Hash independently verified** | 2026-07-31 against the archive in `DREAM3D_Data/TestFiles` | +| **Used by tests** | `SimplnxCore::RemoveFlaggedFeaturesFilter: Class 4 Invariants - Small IN100 remove small features with fill`; consumers below | +| **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) | -The commit author and co-authors establish who introduced the archive reference to the repository, but they do not establish who executed the embedded generating pipeline. The generating engineer is therefore recorded as unknown. +The first registration in `src/Plugins/SimplnxCore/test/CMakeLists.txt` is `c3be139e2`, authored by Joey Kleingers. The local archive SHA512 matches the current registration. The archive contains `6_5_test_data_1_v2/6_5_test_data_1_v2.dream3d`, its XDMF companion and two AppleDouble metadata entries. -## How It Was Generated +| Test source under `src/Plugins/SimplnxCore/` | Archive role | +|---|---| +| `test/ComputeSurfaceFeaturesTest.cpp` | Small IN100 input and archived SurfaceFeatures regression values | +| `test/CreateFeatureArrayFromElementArrayTest.cpp` | Input and archived feature ConfidenceIndex and IPFColors regression values | +| `test/CropImageGeometryTest.cpp` | DataContainer2 and seven cropped geometries for index and physical-bounds checks | +| `test/RequireMinNumNeighborsTest.cpp` | Input only; expected feature values are derived before execution | +| `test/RemoveFlaggedFeaturesTest.cpp` | Input only; flags, compaction expectations and invariants are derived before execution | +| `test/ScalarSegmentFeaturesTest.cpp` | Small IN100 scalar input; checks 847 feature tuples | -The `.dream3d` file contains an embedded DREAM3D-NX pipeline that documents the archive construction. The pipeline reads the predecessor `6_5_test_data_1/6_5_test_data_1.dream3d`, copies `DataContainer` to `DataContainer2`, and changes the copied **Image Geometry** origin to `[-10.0, 50.0, 30.0]` and spacing to `[0.5, 0.5, 0.5]`. It then runs `CropImageGeometryFilter` for all seven nonempty combinations of the X, Y, and Z crop dimensions and writes the v2 `.dream3d` file. +## How it was generated -No standalone copy of the generating pipeline was committed to this repository. The embedded pipeline is the available generation record. The exact DREAM3D-NX build and the person who executed it are unknown. +The `.dream3d` file stores the generating pipeline at `/Pipeline/Pipeline`. Its nested imported pipeline reads `6_5_test_data_1/6_5_test_data_1.dream3d`, copies `DataContainer` to `DataContainer2`, and sets the copy's origin to `[-10,50,30]` and spacing to `[0.5,0.5,0.5]`. The outer pipeline rebuilds all seven nonempty combinations of crop axes with feature renumbering enabled and writes the v2 file. Enabled crop axes use inclusive bounds `[10,15,0]` through `[60,40,50]`. -## Archive Contents +The archive supplies measured Small IN100 data, not synthetic expected removal output. The following input arrays retain their original values until the tests execute the filter. -The archive contains one DREAM3D-NX-format `.dream3d` file, its `.xdmf` companion, and two macOS AppleDouble metadata entries prefixed with `._`. The principal **Image Geometries** are: +| DataPath | Shape | Type | Range | +|---|---|---|---| +| `DataContainer/CellData/FeatureIds` | `[100,100,100,1]` | int32 | 1 through 846 | +| `DataContainer/CellFeatureData/NumElements` | `[847,1]` | int32 | 0 through 21568 | +| `DataContainer/CellFeatureData/NumNeighbors` | `[847,1]` | int32 | 0 through 42 | +| `DataContainer/CellFeatureData/Phases` | `[847,1]` | int32 | 0 through 1 | -| DataPath | Dimensions | Origin | Spacing | Role | -|---|---:|---:|---:|---| -| `DataContainer` | `[100, 100, 100]` | `[0.0, 0.0, 0.0]` | `[1.0, 1.0, 1.0]` | Original Small IN100 input and legacy feature arrays | -| `DataContainer2` | `[100, 100, 100]` | `[-10.0, 50.0, 30.0]` | `[0.5, 0.5, 0.5]` | Rescaled input for crop tests | -| `6_5_Cropped_ImageGeom` | `[51, 26, 51]` | `[-5.0, 57.5, 30.0]` | `[0.5, 0.5, 0.5]` | X, Y, and Z crop reference | -| `6_5_Cropped_XY_ImageGeom` | `[51, 26, 100]` | `[-5.0, 57.5, 30.0]` | `[0.5, 0.5, 0.5]` | X and Y crop reference | -| `6_5_Cropped_XZ_ImageGeom` | `[51, 100, 51]` | `[-5.0, 50.0, 30.0]` | `[0.5, 0.5, 0.5]` | X and Z crop reference | -| `6_5_Cropped_YZ_ImageGeom` | `[100, 26, 51]` | `[-10.0, 57.5, 30.0]` | `[0.5, 0.5, 0.5]` | Y and Z crop reference | -| `6_5_Cropped_X_ImageGeom` | `[51, 100, 100]` | `[-5.0, 50.0, 30.0]` | `[0.5, 0.5, 0.5]` | X crop reference | -| `6_5_Cropped_Y_ImageGeom` | `[100, 26, 100]` | `[-10.0, 57.5, 30.0]` | `[0.5, 0.5, 0.5]` | Y crop reference | -| `6_5_Cropped_Z_ImageGeom` | `[100, 100, 51]` | `[-10.0, 50.0, 30.0]` | `[0.5, 0.5, 0.5]` | Z crop reference | +The RemoveFlaggedFeatures comparison inputs were separately constructed by `vv_work:RemoveFlaggedFeaturesFilter/make_ab.py`. The script writes matching legacy-native inputs and paired pipelines for fixtures A through F. `vv_work:RemoveFlaggedFeaturesFilter/ReadMe.md` identifies the stock DREAM.3D v6.5.171 PipelineRunner and records the comparison date as 2026-09-02. The saved NX runner logs have timestamps on 2026-09-03. -For the `RequireMinNumNeighborsFilter` regression, the relevant input arrays are: +For that comparison, 25 of 25 removal-array comparisons match DREAM.3D v6.5.171 and the analytical expectations. Fixture B extraction also matches in dimensions, origin and the three cell arrays. These results describe the comparison code recorded as `abd495a4a` in the ReadMe, not the current algorithm. -| DataPath | Shape | Type | Observed range | -|---|---:|---|---| -| `DataContainer/CellData/FeatureIds` | `[100, 100, 100, 1]` | `int32` | 1 through 846 | -| `DataContainer/CellFeatureData/NumElements` | `[847, 1]` | `int32` | 0 through 21568 | -| `DataContainer/CellFeatureData/NumNeighbors` | `[847, 1]` | `int32` | 0 through 42 | -| `DataContainer/CellFeatureData/Phases` | `[847, 1]` | `int32` | 0 through 1 | +| Fixture | Recorded result | Artifact | +|---|---|---| +| A, B, E: five removal cases | FeatureIds, CellValue, IgnoredValue, Int32DataSet and Active match exactly | `vv_work:RemoveFlaggedFeaturesFilter/ReadMe.md` | +| B: extraction | Both produce dimensions (2,1,1), origin (1,3,0) and equal cell arrays; names and feature-data packaging differ (D7) | `vv_work:RemoveFlaggedFeaturesFilter/legacy_B_extract.json`, `vv_work:RemoveFlaggedFeaturesFilter/nx_B_extract.d3dpipeline` | +| C: no fill source | DREAM.3D times out after 30 seconds; SIMPLNX returns `-45436` (D3) | `vv_work:RemoveFlaggedFeaturesFilter/log_6_5_171_C_hang.txt`, `vv_work:RemoveFlaggedFeaturesFilter/log_nx_C_hang.txt` | +| D: ID outside feature range | DREAM.3D completes and the ReadMe records a zeroed cell; SIMPLNX returns `-45435` (D4) | `vv_work:RemoveFlaggedFeaturesFilter/log_6_5_171_D_range.txt`, `vv_work:RemoveFlaggedFeaturesFilter/log_nx_D_range.txt` | +| F: empty flagged feature | DREAM.3D produces a false one-cell crop; SIMPLNX skips the feature with `-53905` (D6) | `vv_work:RemoveFlaggedFeaturesFilter/ReadMe.md`, `vv_work:RemoveFlaggedFeaturesFilter/log_nx_F_extract_empty.txt` | -## Test Consumers +Algorithm history after the recorded run date includes `4ec21e93f` (2026-09-20, Direct/Scanline architecture), `2de2d44db` (2026-09-23, V&V fixes), `a33c370ca` (2026-09-25, messaging API) and `62000012e` (2026-10-20, progress reporting). The report therefore does not present the earlier matches as a comparison of the current algorithm. -| Test source | Active test cases | Archive role | -|---|---:|---| -| `test/ComputeSurfaceFeaturesTest.cpp` | 1 | Supplies the Small IN100 input and archived `SurfaceFeatures` legacy regression values | -| `test/CreateFeatureArrayFromElementArrayTest.cpp` | 2 | Supplies the Small IN100 input and archived feature-level `ConfidenceIndex` and `IPFColors` regression values | -| `test/CropImageGeometryTest.cpp` | 14 | Supplies `DataContainer2` and the seven cropped geometries used as regression references for voxel-index and physical-bounds variants | -| `test/RequireMinNumNeighborsTest.cpp` | 1 | Supplies input data only; expected retained `NumElements` values are derived in the test before the filter runs | -| `test/RemoveFlaggedFeaturesTest.cpp` | 1 | Supplies input data only for the Class 4 invariant test; the flag array (features with fewer than 100 cells), the expected compaction and every invariant are derived from the pre-filter arrays | -| `test/ScalarSegmentFeaturesTest.cpp` | 1 | Supplies the Small IN100 scalar input; the test checks that 847 feature tuples are produced | +## Canonical oracle output -## Canonical Oracle Output +No archive array is a golden removal output for RemoveFlaggedFeatures or RequireMinNumNeighbors. Those tests derive expectations from the input. Other consumers use the archived SurfaceFeatures, ConfidenceIndex, IPFColors and cropped geometries as regression references. -No array in this archive is the canonical correctness oracle for `RequireMinNumNeighborsFilter`. The archive is an input fixture for that test. The expected `NumElements` sequence is derived in the test from the pre-filter `NumElements` values and the independently computed neighbor-count threshold, while the primary V&V oracle is the inline Class 1 analytical fixture with Class 4 invariants. +| DataPath | Source of expected values | +|---|---| +| `DataContainer/CellData/FeatureIds` | Class 1 literal IDs for the constructed fixtures; Class 4 surviving-ID and compaction checks for Small IN100 | +| `DataContainer/CellData/CellValue` | Class 1: start at `100 + cell index`; each filled value identifies its chosen neighbor | +| `DataContainer/CellData/IgnoredValue` | Class 1: start at `500 + cell index`; preserve values when ignored, otherwise copy the selected neighbor | +| `DataContainer/CellFeatureData/Int32DataSet` | Class 1: start at `1000 + feature ID`; preserve surviving tuples in original order | +| `DataContainer/CellFeatureData/NumElements` and `Phases` | Class 4: copy the surviving input tuples; feature values are not recomputed | +| Extracted geometry and cell arrays | Class 1 bounding-box dimensions, origin, spacing and literal cell values | -Some other tests use arrays or geometries in this archive as legacy regression references: +`src/Plugins/SimplnxCore/test/RemoveFlaggedFeaturesTest.cpp` encodes the independent oracle. The eight Class 1 cases use the following fixtures. -| DataPath | Role | Oracle classification | +| Fixture | Definition | Expected result | |---|---|---| -| `DataContainer/CellFeatureData/SurfaceFeatures` | `ComputeSurfaceFeaturesFilter` legacy regression reference | Not an independent V&V oracle | -| `DataContainer/CellFeatureData/ConfidenceIndex` | `CreateFeatureArrayFromElementArrayFilter` legacy regression reference | Not an independent V&V oracle | -| `DataContainer/CellFeatureData/IPFColors` | `CreateFeatureArrayFromElementArrayFilter` legacy regression reference | Not an independent V&V oracle | -| `6_5_Cropped_*` geometries | `CropImageGeometryFilter` regression references generated by the embedded pipeline | Not an independent V&V oracle | +| A | 5×2×1; remove features 2 and 3 | Filled IDs `[1,1,1,1,1,1,2,2,2,2]`; distinct-neighbor, single-neighbor, tie and two-iteration copies | +| B | 4×4×1 with five background cells; remove feature 3 | Filled IDs `[0,1,1,1,1,0,2,2,2,2,0,1,2,2,0,0]`; cell 13 copies cell 12, cell 14 copies background cell 15 | +| E | 3×3×3; remove features 3 and 4 | All six face directions; center cell 13 copies +Z cell 22 after a 4-to-2 vote; cell 0 fills in a second iteration | +| Multiple extraction | 12 feature tuples; extract features 3 and 11 | Names end in `-03` and `-11`; each crop has the hand-derived size, origin and cell values | + +Fixture B extraction gives dimensions (2,1,1), origin (1,3,0), unit spacing, FeatureIds `[3,3]`, CellValue `[113,114]` and IgnoredValue `[513,514]`. Current tests also assert that the Feature Attribute Matrix is copied unchanged and `tempBounds` is absent from source and output. + +The Small IN100 test flags 275 features with fewer than 100 cells. It checks feature compaction, NeighborList deletion, surviving IDs, unchanged-cell renumbering, cell/feature phase agreement and a same-ID face neighbor for each filled cell. It checks 8,535 filled cells and requires surviving-feature growth to equal that count. + +The 28 current test cases run in Linux, macOS and Windows in-core CI. All pass in CI at `419f9db35`. + +Release exposure was checked in the source at every `upstream/release/v` from v7.0.0 through v7.4.2. The relevant sources are `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/RemoveFlaggedFeatures.cpp`, `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/RemoveFlaggedFeaturesFilter.cpp` and, in v7.4.2, `src/Plugins/SimplnxCore/src/SimplnxCore/utils/FeatureRemovalUtilities.cpp`. + +| IDs | Release-source finding | +|---|---| +| D1 | v7.0.0 through v7.4.1 omit the first neighbor hit; v7.4.2 contains the correction from PR #1700. | +| D2, D3, D4 | v7.0.0 through v7.4.2 retain background targeting and omit progress, ID-range and geometry-count guards. | +| D5, D6 | v7.0.0 through v7.4.2 test the wrong delegated result and omit empty-bounds checks. | +| D8 | v7.0.0 through v7.4.2 leave the temporary bounds in the DataStructure during crops. | +| D9 | v7.0.0 through v7.4.2 emit `-11505` and also call the NeighborList removal helper. | + +DREAM.3D source at tag `v6.5.171` is `Source/Plugins/Processing/ProcessingFilters/RemoveFlaggedFeatures.cpp` and `Source/Plugins/Sampling/SamplingFilters/ExtractFlaggedFeatures.cpp`. It counts the first neighbor hit and only fills negative IDs (D1/D2 absent). It has no fill-progress or ID-range guard (D3/D4 present), initializes empty bounds to -1 (D6 present), stores bounds privately (D8 absent), and has neither the SIMPLNX Result-based check nor the duplicate warnings (D5/D9 absent). + +The current implementation lives in `RemoveFlaggedFeaturesDirect.cpp` and `RemoveFlaggedFeaturesScanline.cpp`, with shared crop delegation in `RemoveFlaggedFeaturesCommon.hpp`. It no longer calls `FeatureRemovalUtilities::removeFlaggedFeatures()`. PR #1736 (`2de2d44db`) contains the V&V fixes and the bounds/warning cleanup. The current extraction tests preserve the source Feature Attribute Matrix in the crops; they do not remove it. -## Oracle Provenance +## Oracle provenance (Classes 2, 3, 5 only) -Not applicable. This shared archive does not provide a Class 2, Class 3, or Class 5 canonical oracle for `RequireMinNumNeighborsFilter`. Its valid role in that V&V is limited to input and regression coverage. +Not applicable. The independent expectations are Classes 1 and 4 and are encoded in the test source. The legacy comparison explains migration differences. -## Second-Engineer Oracle Review +## Second-engineer oracle review -- **Reviewer:** skipped -- **Date:** not applicable -- **Skip reason:** The archive is not the canonical oracle for `RequireMinNumNeighborsFilter`; the separate inline Class 1 and Class 4 oracle is subject to the filter report's second-engineer review. +- **Reviewer:** pending for RemoveFlaggedFeaturesFilter. +- **Date:** pending. -## Regenerated to Fix a Circular-Oracle Situation? +The shared archive is input only for the RequireMinNumNeighbors oracle; its inline Class 1 and Class 4 expectations have their review recorded in that filter's report. -No evidence indicates that this archive was regenerated to fix a circular oracle. Git history shows that v2 replaced `6_5_test_data_1.tar.gz` in PR #1021 while the crop-dimension test variants and their generated reference geometries were added. +## Regenerated to fix a circular-oracle situation? -The archive was not regenerated during the `RequireMinNumNeighborsFilter` V&V cycle and its arrays are not used as post-filter golden output for that filter. The previously hard-coded `NumElements` snapshot was a source-level test constant, not a regenerated archive; the current test derives the expectation from pre-filter inputs instead. +No. PR #1021 replaced `6_5_test_data_1.tar.gz` with the v2 archive while adding crop-dimension regression coverage. RemoveFlaggedFeatures and RequireMinNumNeighbors derive their expected results from input values and independent rules, not from post-filter archive outputs. From dfa21ae1cad49bc96f668cbe98db7b8cdcc1ab67 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 20:01:29 -0400 Subject: [PATCH 59/91] DOC: Rewrite the ReplaceElementAttributesWithNeighborValues V&V record to follow the report template Signed-off-by: Michael Jackson --- ...ementAttributesWithNeighborValuesFilter.md | 167 +++++++++--------- ...ementAttributesWithNeighborValuesFilter.md | 79 +++++++-- ...laceElementAttributesWithNeighborValues.md | 110 ++++++------ 3 files changed, 202 insertions(+), 154 deletions(-) diff --git a/src/Plugins/SimplnxCore/vv/ReplaceElementAttributesWithNeighborValuesFilter.md b/src/Plugins/SimplnxCore/vv/ReplaceElementAttributesWithNeighborValuesFilter.md index d1611a85b0..22dc2619aa 100644 --- a/src/Plugins/SimplnxCore/vv/ReplaceElementAttributesWithNeighborValuesFilter.md +++ b/src/Plugins/SimplnxCore/vv/ReplaceElementAttributesWithNeighborValuesFilter.md @@ -1,118 +1,119 @@ # V&V Report: ReplaceElementAttributesWithNeighborValuesFilter -| | | -|-----------|------------------| -| Plugin | SimplnxCore | -| SIMPLNX UUID | `65128c53-d3be-4a69-a559-32a48d603884` | -| SIMPLNX Human Name | Replace Element Attributes with Neighbor (Threshold) | -| DREAM3D 6.5.171 equivalent | `ReplaceElementAttributesWithNeighborValues` — SIMPL UUID `17410178-4e5f-58b9-900e-8194c69200ab` | -| Verified commit | ** | -| Status | **COMPLETE** | -| Sign-off | Nathan Young, 07-21-2026 | +| | | +|--------|--------------| +| Plugin | SimplnxCore | +| SIMPLNX UUID | `65128c53-d3be-4a69-a559-32a48d603884` | +| SIMPLNX Human Name | Replace Element Attributes with Neighbor (Threshold) | +| DREAM.3D v6.5.171 equivalent | `ReplaceElementAttributesWithNeighborValues` (OrientationAnalysis plugin, SIMPL UUID `17410178-4e5f-58b9-900e-8194c69200ab`) | +| Verified commit | ** | +| Status | COMPLETE | +| Sign-off | Nathan Young, 2026-07-21; second engineer: Matthew Marine, 2026-07-22 | ## At a glance -| Aspect | Current state | -|------------------------|--------------------------| -| Algorithm Relationship | **Port** — direct translation of SIMPL `ReplaceElementAttributesWithNeighborValues`; same two-pass (scan → copy) + optional-loop structure. Legacy UUID cited in filter header. | -| Oracle | **Class 1 (Analytical)** — synthetic 3×3×3 grid, 4 SECTIONs with closed-form expected values. **Class 4 (Invariant)** companion — post-loop threshold saturation and multi-array copy. | -| Code paths | 16 of 17 directly tested; cancel path untestable (no cancel-signal injection in test framework). | -| Tests | 2 TEST_CASEs: synthetic (4 SECTIONs, Class 1 + Class 4 oracle) + SIMPL backwards-compat (2 DYNAMIC_SECTIONs). | -| Exemplar archive | `6_6_replace_element_attributes_with_neighbor.tar.gz` — unreferenced; consuming circular-oracle test removed. See `vv/provenance/ReplaceElementAttributesWithNeighborValues.md`. | -| Legacy comparison | **Complete (2026-07-21, re-run 2026-07-21 post-refactor).** Bit-identical on LessThan and GreaterThan vs 6.5.171; a locally patched legacy build also matched 6.5.171 bit-for-bit on LessThan. No deviations. See `vv/deviations/ReplaceElementAttributesWithNeighborValuesFilter.md`. | -| Bug flags | None. | +| Aspect | Current state | +|--------|---------------| +| Algorithm Relationship | Minor changes to `ReplaceElementAttributesWithNeighborValues` in DREAM.3D v6.5.171. SIMPLNX retains the face-neighbor rules, preserves the comparison value's type, and transfers numeric tuples one Z slice at a time. | +| Oracle (confirmed) | Classes 1 and 4 use four synthetic sections in `test/ReplaceElementAttributesWithNeighborValuesTest.cpp`. All pass in CI. | +| Code paths enumerated | 20 of 30 paths exercised. Gaps include cancellation, I/O failures, nonnumeric siblings, other comparison types, and a loop with no passing values. | +| Tests today | 4 test cases: synthetic replacement, archive regression, SIMPL conversion, and a hidden data generator. The synthetic case has four sections. | +| Exemplar archive | `6_6_replace_element_attributes_with_neighbor.tar.gz` is used by the archive regression. The test loads the same file as input and reference; the independent oracle is in the synthetic test. | +| Legacy comparison | Not run on the current algorithm. The comparison dated 2026-07-21 predates the change to slice transfers. | +| Bug flags | `ReplaceElementAttributesWithNeighborValuesFilter-D3` | +| V&V phase | COMPLETE. Two defects are fixed; the loop-termination defect remains open. | ## Summary -`ReplaceElementAttributesWithNeighborValuesFilter` replaces each "bad" cell (one that fails a user-supplied threshold comparison) by copying all AttributeMatrix arrays from the best-passing face neighbor. Algorithm: (1) scan pass — for each bad voxel, check 6 face neighbors and record the one with the most-extreme passing value; (2) copy pass — apply recorded neighbor copies to all cell arrays. An optional loop repeats until no bad voxels remain. - -Verification used a Class 1 (Analytical) synthetic 3×3×3 fixture with closed-form expected values across four modes (LessThan, GreaterThan, loop=false, compare1=false), paired with Class 4 (Invariant) post-loop threshold saturation and multi-array copy assertions. A/B comparison against DREAM3D 6.5.171 confirmed bit-identical output on both operators; a locally patched legacy build also matched 6.5.171 bit-for-bit on LessThan. The comparison was re-run after the algorithm was refactored and remained bit-identical. No deviations. +`ReplaceElementAttributesWithNeighborValuesFilter` replaces a cell that fails the threshold by copying numeric tuples from the best passing face neighbor. Four synthetic sections check analytical values, threshold saturation, and copying to a second array; all pass in CI. Two defects are fixed, but loop mode does not terminate when all comparison values fail the threshold. ## Algorithm Relationship -*Classification:* **Port** - -Near line-by-line translation of SIMPL `ReplaceElementAttributesWithNeighborValues`. Legacy UUID `17410178-4e5f-58b9-900e-8194c69200ab` cited explicitly in `ReplaceElementAttributesWithNeighborValuesFilter.hpp`. SIMPL conversion fixtures present at `test/simpl_conversion/6_5/` and `6_4/ReplaceElementAttributesWithNeighborValuesFilter.json`. Two-pass architecture, 6-face-neighbor scan with individual boundary guards, and `IComparisonFunctor` dispatch pattern are direct ports. +*Classification:* Minor changes -*Port-time deltas (none behavioral):* - -1. **Algorithm class extraction** — core logic moved to `Algorithms/ReplaceElementAttributesWithNeighborValues.cpp` per SimplnxCore convention. -2. **Geometry access** — uses `NeighborUtilities::initializeFaceNeighborOffsets(dims)`; equivalent to legacy's hard-coded `int64 neighpoints[6]`. -3. **Array copy pass** — iterates `AttributeMatrix` with structured binding + `copyTuple`; legacy iterated SIMPL `AttributeMatrix` equivalently. -4. **Type dispatch** — `ExecuteDataFunction(ExecuteTemplate{}, ...)` vs SIMPL's `EXECUTE_FUNCTION_TEMPLATE`. UX/framework only. -5. **Progress reporting** — `ProgressMessage` via `messageHandler` vs `notifyStatusMessage`. UX only. +*Evidence:* `src/Plugins/SimplnxCore/src/SimplnxCore/SimplnxCoreLegacyUUIDMapping.hpp` maps the legacy UUID to this filter; both implementations use six face neighbors, while SIMPLNX preserves the candidate type and transfers one Z slice at a time. ## Oracle -*Class:* **1 (Analytical)** primary + **4 (Invariant)** companion. Classes 2, 3, 5 N/A — no external library implements this heuristic; no published algorithm. - -### Class 1 — Analytical derivations +*Class:* 1 (Analytical), with 4 (Invariant). -3×3×3 grid, float32 `Confidence Index`, int32 `Marker` (initialized to linear index): +*Applied:* Hand-derived values on a 3×3×3 grid specify replacement of isolated bad cells and preservation of a center with no passing neighbor; invariants check threshold saturation and a changed marker. -- **LessThan** (`compare = val < threshold`), bad={0,13,26}, good=0.9, bad=0.1, threshold=0.5, loop=true: bad voxels fail (0.1 < 0.5). Each has ≥1 face neighbor at 0.9. `compare1(0.9 ≥ 0.5)` = true; `compare2(0.9 > 0.1)` = true on first good neighbor; remaining equal neighbors fail `compare2(0.9 > 0.9)`. After copy pass: all three become 0.9. Second iteration: count=0, exit. -- **GreaterThan** (`compare = val > threshold`): symmetric with good=0.1, bad=0.9. All three become 0.1. All values ≤ 0.5 after one iteration. -- **loop=false**: single bad voxel at center (index 13), 6 good neighbors, one pass fills it. Deterministic. -- **compare1=false**: bad cluster = center (13) + all 6 face neighbors {4,10,12,14,16,22}. In the scan pass every `compare1` call for voxel 13 returns false → `bestNeighbor[13]` stays -1 → voxel 13 unchanged. Ring voxels each have ≥1 good face neighbor outside the cluster and are replaced. +*Encoded:* `src/Plugins/SimplnxCore/test/ReplaceElementAttributesWithNeighborValuesTest.cpp::SimplnxCore::ReplaceElementAttributesWithNeighborValuesFilter: Synthetic neighbor replacement` — four fixture sections. All pass in CI. -Multi-array copy verified via `Marker`: after replacement `markerStore[13] != 13`, confirming the copy pass covers all AttributeMatrix arrays, not just the comparison array. +*Second-engineer review:* Matthew Marine, 2026-07-22. -### Class 4 — Invariants +## Bugs found and fixed -- Post-loop threshold saturation: after `loop=true` convergence, every value passes the threshold. -- Multi-array copy: `markerStore[13] != 13` after bad-center-voxel replacement. +All defects in this table are fixed. DREAM3D-NX v7.4.2 contains both fixes. -### Encoded - -`test/ReplaceElementAttributesWithNeighborValuesTest.cpp` — TEST_CASE `"Synthetic neighbor replacement"` — 4 SECTIONs with explicit `REQUIRE` checks. All pass. - -*Second-engineer review:* Skipped — no second engineer available at time of V&V. +| Deviation | Defect | Affected released versions | Resolution | +|-----------|--------|----------------------------|------------| +| `ReplaceElementAttributesWithNeighborValuesFilter-D1` | A float32 temporary could change the selected neighbor for a float64 or large-integer comparison array. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | The best candidate retains the comparison array's numeric type. | +| `ReplaceElementAttributesWithNeighborValuesFilter-D2` | A nonnumeric sibling in the cell AttributeMatrix could cause a failed reference cast during replacement. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | The filter copies numeric `IDataArray` siblings and skips other objects. | ## Code path coverage -*16 of 17 paths directly tested; 1 untestable gap.* - -Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReplaceElementAttributesWithNeighborValues.cpp` - -| # | Phase | Path | Test coverage | -|----|--------------|----------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------| -| 1 | Loop | Cancel check at top of `while(keepGoing)` | **Untestable** — no cancel-signal injection available. | -| 2 | (a) Scan | Good voxel → skip; `count` not incremented | All 24 good voxels in each synthetic SECTION. | -| 3 | (a) Scan | Bad voxel → enter 6-neighbor search; `count++`| LessThan/GreaterThan: voxels {0,13,26}; loop=false: voxel 13. | -| 4 | (a) Scan | -Z boundary guard (`plane == 0`) → skip | Voxel 0 at (0,0,0). | -| 5 | (a) Scan | -Y boundary guard (`row == 0`) → skip | Voxel 0 at (0,0,0). | -| 6 | (a) Scan | -X boundary guard (`column == 0`) → skip | Voxel 0 at (0,0,0). | -| 7 | (a) Scan | +X boundary guard (`column == dims[0]-1`) → skip | Voxel 26 at (2,2,2). | -| 8 | (a) Scan | +Y boundary guard (`row == dims[1]-1`) → skip | Voxel 26 at (2,2,2). | -| 9 | (a) Scan | +Z boundary guard (`plane == dims[2]-1`) → skip | Voxel 26 at (2,2,2). | -| 10 | (a) Scan | `compare1=true` AND `compare2=true` → update `best` + `bestNeighbor[i]` | First good face neighbor of each bad voxel. | -| 11 | (a) Scan | `compare1=false` → skip neighbor | compare1=false SECTION: cluster bad={4,10,12,13,14,16,22}; all neighbors of voxel 13 fail compare1; `bestNeighbor[13]` stays -1. | -| 12 | (a) Scan | `compare1=true` AND `compare2=false` → skip neighbor | Voxel 13: first good neighbor sets `best=0.9`; remaining 5 at 0.9 fail `compare2(0.9 > 0.9)`. | -| 13 | (b) Copy | `bestNeighbor[i] != -1` → `copyTuple` across all AttributeMatrix arrays | LessThan: voxels {0,13,26} replaced; `markerStore[13] != 13` confirms non-comparison array copied. | -| 14 | (b) Copy | `bestNeighbor[i] == -1` → skip | All 24 good voxels; also voxel 13 in the compare1=false SECTION. | -| 15 | Loop control | `loopUntilDone && count > 0` → `keepGoing = true` | LessThan: iteration 1 has count=3; sets keepGoing=true. | -| 16 | Loop control | `count == 0` → exit while | LessThan: second iteration has count=0. | -| 17 | Loop control | `loop=false` → single pass only | loop=false SECTION. | +20 of 30 paths exercised. Each uncovered path has a reason below. + +Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReplaceElementAttributesWithNeighborValues.cpp` (368 lines); `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReplaceElementAttributesWithNeighborValuesFilter.cpp` (140 lines). + +Rows that name a section refer to `SimplnxCore::ReplaceElementAttributesWithNeighborValuesFilter: Synthetic neighbor replacement`. Each pass selects neighbors from a rolling comparison window and transfers each completed Z slice. + +| # | Phase | Path | Test case | +|---|-------|------|-----------| +| 1 | Loop | Cancel before a pass | *Not directly tested. Requires cancellation injection.* | +| 2 | Scan | A passing cell skips the neighbor search | `LessThan replaces low voxels and loops until none remain` | +| 3 | Scan | A failing cell increments the count and searches neighbors | `LessThan replaces low voxels and loops until none remain`; `GreaterThan replaces high voxels and loops until none remain` | +| 4 | Scan | Reject the -Z neighbor at the boundary | `LessThan replaces low voxels and loops until none remain` — cell 0 | +| 5 | Scan | Reject the -Y neighbor at the boundary | `LessThan replaces low voxels and loops until none remain` — cell 0 | +| 6 | Scan | Reject the -X neighbor at the boundary | `LessThan replaces low voxels and loops until none remain` — cell 0 | +| 7 | Scan | Reject the +X neighbor at the boundary | `LessThan replaces low voxels and loops until none remain` — cell 26 | +| 8 | Scan | Reject the +Y neighbor at the boundary | `LessThan replaces low voxels and loops until none remain` — cell 26 | +| 9 | Scan | Reject the +Z neighbor at the boundary | `LessThan replaces low voxels and loops until none remain` — cell 26 | +| 10 | Scan | A passing, better candidate becomes the selected source | `LessThan replaces low voxels and loops until none remain` | +| 11 | Scan | Reject a neighbor that fails the threshold | `bad voxel surrounded only by bad neighbors is not replaced (compare1=false skip)` | +| 12 | Scan | Reject a passing neighbor whose value equals the best value | `LessThan replaces low voxels and loops until none remain` — equal candidates at cell 13 | +| 13 | Copy | Copy a selected source into the comparison and marker arrays | `LessThan replaces low voxels and loops until none remain` — exact replaced values and changed center marker | +| 14 | Copy | Preserve cells with no selected source | `bad voxel surrounded only by bad neighbors is not replaced (compare1=false skip)` — center stays 0.1 | +| 15 | Loop | Repeat when loop mode is enabled and the pass counted failing cells | `LessThan replaces low voxels and loops until none remain` | +| 16 | Loop | Exit after a pass counts no failing cells | `LessThan replaces low voxels and loops until none remain`; `GreaterThan replaces high voxels and loops until none remain` | +| 17 | Loop | Stop after one pass when loop mode is disabled | `loop disabled performs a single pass`; `bad voxel surrounded only by bad neighbors is not replaced (compare1=false skip)` | +| 18 | Setup | Select LessThan and prefer larger passing values | `LessThan replaces low voxels and loops until none remain` | +| 19 | Setup | Select GreaterThan and prefer smaller passing values | `GreaterThan replaces high voxels and loops until none remain` | +| 20 | Slices | Read initial slices, rotate the window, read the next slice, and reset source marks after each transfer | `bad voxel surrounded only by bad neighbors is not replaced (compare1=false skip)`; both loop sections | +| 21 | Slices | Process a single Z slice without reading a second slice | *Not directly tested. Synthetic fixtures have three slices; the archive regression does not isolate this condition.* | +| 22 | I/O | Return a first-slice comparison read error | *Not directly tested. Requires an injected read failure.* | +| 23 | I/O | Return a second-slice comparison read error | *Not directly tested. Requires an injected read failure.* | +| 24 | I/O | Return a comparison read error after window rotation | *Not directly tested. Requires an injected read failure.* | +| 25 | I/O | Return a sibling tuple-transfer error | *Not directly tested. Requires an injected transfer read or write failure.* | +| 26 | Copy | Skip a sibling that is not an `IDataArray` | *Not directly tested. No fixture includes a nonnumeric sibling.* | +| 27 | Loop | Cancel after a completed pass | *Not directly tested. Requires cancellation injection.* | +| 28 | Dispatch | Compare boolean, integer, or float64 values instead of float32 | *Not directly tested. All executing test cases select a float32 comparison array.* | +| 29 | Loop | Repeat without progress when every value fails the threshold | *Not directly tested. An uncancelled fixture would not terminate; see `ReplaceElementAttributesWithNeighborValuesFilter-D3`.* | +| 30 | Preflight | Accept the comparison array and declare its parent contents modified | `Synthetic neighbor replacement` — preflight succeeds in all four sections | ## Test inventory -| Test case (SECTION) | Oracle | Parameters | -|----------------------------------------------|---------------|-------------------------------------------------------------------------| -| Synthetic — LessThan | Class 1 + 4 | bad={0,13,26}, good=0.9, threshold=0.5, loop=true. All 27 ≥ threshold; `Marker[13]` copied. | -| Synthetic — GreaterThan | Class 1 + 4 | bad={0,13,26}, good=0.1, threshold=0.5, loop=true. All 27 ≤ threshold. | -| Synthetic — loop=false | Class 1 | bad={13}, good=0.9, threshold=0.5, loop=false. Single pass fills center. | -| Synthetic — compare1=false skip | Class 1 | bad cluster={4,10,12,13,14,16,22}, loop=false. Center stays bad; ring replaced. Covers path 11. | -| SIMPL Backwards Compatibility (6.4 + 6.5) | N/A | UUID + arg-key + value round-trip via `Pipeline::FromSIMPLFile`. Not an oracle test. | +The test cases are in `src/Plugins/SimplnxCore/test/ReplaceElementAttributesWithNeighborValuesTest.cpp`. The three ordinary cases pass in CI. The generator is hidden. + +| Test case | Status | Notes | +|-----------|--------|-------| +| `SimplnxCore::ReplaceElementAttributesWithNeighborValuesFilter: Generate Test Data` | kept | Hidden (`[.GenerateTestData]`). Writes patterned 20×20×20 and 200×200×200 inputs with confidence, Euler-angle, and phase arrays; does not execute the filter. | +| `SimplnxCore::ReplaceElementAttributesWithNeighborValuesFilter` | kept | Loads the same archive file as input and reference, runs LessThan at 0.1 with loop enabled, compares cell arrays, and checks tuple dimensions. This checks stability on the stored data. | +| `SimplnxCore::ReplaceElementAttributesWithNeighborValuesFilter: Synthetic neighbor replacement` | new-for-V&V | Four sections check LessThan, GreaterThan, one pass, and a bad center surrounded by bad neighbors. Checks all 27 values against the threshold in the loop sections, exact replacement values, and a changed center marker. | +| `SimplnxCore::ReplaceElementAttributesWithNeighborValuesFilter: SIMPL Backwards Compatibility` | kept | `SIMPL 6.5 (UUID)` and `SIMPL 6.4 (Filter_Name)` sections check identity, comments, threshold, comparison, loop, and both input paths. Does not execute replacement. | ## Exemplar archive - **Archive:** `6_6_replace_element_attributes_with_neighbor.tar.gz` - **SHA512:** `319ebdf08b83ce5ec915afda8ee2af1e0952a3ce26b3e65a4171eb3125c7bc6613c3994610bf526f70413c00da9061c3e6c2d867643220d62faa8c3cd79a96cd` -- **Status:** Unreferenced — the circular-oracle test that consumed it was removed. Archive may be retired. -- **Provenance:** `vv/provenance/ReplaceElementAttributesWithNeighborValues.md` +- **Provenance:** `src/Plugins/SimplnxCore/vv/provenance/ReplaceElementAttributesWithNeighborValues.md` -## Deviations from DREAM3D 6.5.171 +## Deviations from DREAM.3D v6.5.171 -No deviations. A/B comparison (2026-07-21) confirmed bit-identical output on LessThan and GreaterThan vs 6.5.171; a locally patched legacy build also matched 6.5.171 bit-for-bit on LessThan. +See `vv/deviations/ReplaceElementAttributesWithNeighborValuesFilter.md` for the root cause, affected users, and recommendation. This table lists only Part 1. -Full record: `vv/deviations/ReplaceElementAttributesWithNeighborValuesFilter.md` +| Deviation | Observed difference | +|-----------|---------------------| +| `ReplaceElementAttributesWithNeighborValuesFilter-D1` | SIMPLNX retains the comparison value's numeric type when it selects the best neighbor; DREAM.3D v6.5.171 rounds the candidate to float32. | diff --git a/src/Plugins/SimplnxCore/vv/deviations/ReplaceElementAttributesWithNeighborValuesFilter.md b/src/Plugins/SimplnxCore/vv/deviations/ReplaceElementAttributesWithNeighborValuesFilter.md index 5a2dbf14a5..2f2d8f9afb 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/ReplaceElementAttributesWithNeighborValuesFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/ReplaceElementAttributesWithNeighborValuesFilter.md @@ -1,20 +1,71 @@ -# Deviations from DREAM3D 6.5.171: ReplaceElementAttributesWithNeighborValuesFilter +# Deviations from DREAM.3D v6.5.171: ReplaceElementAttributesWithNeighborValuesFilter + +Part 1 lists the numeric-selection difference from DREAM.3D v6.5.171. Part 2 lists the fixed sibling-cast defect and the unfixed loop-termination defect. All entries use one stable ID sequence. + +--- + +## Part 1 — Differences from DREAM.3D v6.5.171 + +### ReplaceElementAttributesWithNeighborValuesFilter-D1 + +| Field | Value | +|---|---| +| **Deviation ID** | `ReplaceElementAttributesWithNeighborValuesFilter-D1` | +| **Filter UUID** | `65128c53-d3be-4a69-a559-32a48d603884` | +| **Status** | active | + +**Symptom:** A float64 or large-integer comparison array can select a different neighbor in SIMPLNX than in DREAM.3D v6.5.171. + +**Root cause:** Bug. DREAM.3D v6.5.171 stores the best candidate in a `float`; affected DREAM3D-NX releases use `float32`. The comparison then converts this rounded value back to the array type. For LessThan at threshold 0.5, two passing float64 candidates encountered as 1.00000004 then 1.00000002 both round to 1.0 in the temporary. The second candidate replaces the larger first candidate. SIMPLNX now uses `T best` and `T& best`, so it retains the larger value. + +**Affected users:** Users with comparison values that lose precision when converted to float32. DREAM3D-NX v7.0.0 through v7.4.1 were affected. DREAM.3D v6.5.171 was affected. + +**Recommendation:** Trust SIMPLNX. DREAM3D-NX v7.4.2 contains the fix and selects the best candidate in the input array's numeric type. + +**Evidence:** PR #1680 (`4fd0abee9`) changes the temporary and reference from float32 to `T`. Direct inspection of all `upstream/release/v7.0.0` through `upstream/release/v7.4.1` algorithm snapshots finds float32; `upstream/release/v7.4.2` has `T`. The DREAM.3D `v6.5.171` source is `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/ReplaceElementAttributesWithNeighborValues.cpp`. The current test file uses float32 comparison arrays and has no precision-specific regression fixture. + +--- + +## Part 2 — SIMPLNX defects and behavior changes + +### ReplaceElementAttributesWithNeighborValuesFilter-D2 + +| Field | Value | +|---|---| +| **Deviation ID** | `ReplaceElementAttributesWithNeighborValuesFilter-D2` | +| **Filter UUID** | `65128c53-d3be-4a69-a559-32a48d603884` | +| **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:** Replacing a cell could throw `std::bad_cast` when the cell AttributeMatrix also contained a nonnumeric object, such as a `StringArray` or `NeighborList`. + +**Root cause:** Bug. The copy loop applied `dynamic_cast` to every child without checking its type. SIMPLNX now uses a pointer cast and skips null results. It copies numeric `IDataArray` siblings; other children remain unchanged. DREAM.3D v6.5.171 calls the legacy `IDataArray::copyTuple()` interface without the failing SIMPLNX reference cast. + +**Affected users:** Users whose cell AttributeMatrix contains nonnumeric children and at least one cell selected for replacement. + +**Recommendation:** Trust SIMPLNX. Use DREAM3D-NX v7.4.2 or later to avoid this cast failure. Handle nonnumeric sibling values separately if they must follow the copied numeric tuples. + +**Evidence:** PR #1680 (`4fd0abee9`) replaces the unchecked cast with typed child collection. Every release snapshot from `upstream/release/v7.0.0` through `upstream/release/v7.4.1` has the unchecked reference cast; `upstream/release/v7.4.2` has typed collection. The current algorithm retains the protection with a checked pointer cast. The DREAM.3D `v6.5.171` source uses the legacy array interface. No current test fixture includes a nonnumeric sibling. + +--- + +### ReplaceElementAttributesWithNeighborValuesFilter-D3 | Field | Value | -|-------|-------| -| SIMPLNX UUID | `65128c53-d3be-4a69-a559-32a48d603884` | -| SIMPL UUID | `17410178-4e5f-58b9-900e-8194c69200ab` | -| Comparison run | 2026-07-21 (initial); re-run 2026-07-21 after algorithm refactor | -| Full comparison details | `replace_element_attributes_vv/comparison_report.md` | +|---|---| +| **Deviation ID** | `ReplaceElementAttributesWithNeighborValuesFilter-D3` | +| **Filter UUID** | `65128c53-d3be-4a69-a559-32a48d603884` | +| **Status** | active | +| **Affected released versions** | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was affected. | +| **Fixed in** | Not fixed | + +**Symptom:** With loop mode enabled, an image whose comparison values all fail the threshold runs until cancelled. -## No deviations observed +**Root cause:** Bug. The loop counts failing cells, not completed replacements. If no neighbor passes the threshold, no value changes, but `loopUntilDone && count > 0` starts another pass. The algorithm has no check for a pass without progress and no iteration limit. -A/B run confirmed **bit-identical output** between SIMPLNX and DREAM3D 6.5.171 on all tested configurations. The comparison was re-run after the algorithm was refactored (`T best` replacing `float32 best`, `bestNeighbor` reset between iterations, precomputed `neighborAMArrays` replacing per-copy `dynamic_cast`) and produced the same results. +**Affected users:** Users who enable Loop Until Gone on data with no passing comparison value, or on a failing region that cannot receive a passing value. -| Test case | Comparison | Result | -|-----------|------------|--------| -| LessThan (index=0), threshold=0.5, loop=true | 6.5.171 vs NX | Bit-identical (125/125 voxels, ConfidenceIndex + Marker) | -| GreaterThan (index=1), threshold=0.5, loop=true | 6.5.171 vs NX | Bit-identical (125/125 voxels, ConfidenceIndex + Marker) | -| LessThan, threshold=0.5, loop=true | Locally patched legacy build vs 6.5.171 | Bit-identical (125/125 voxels) | +**Recommendation:** Disable Loop Until Gone for such inputs. A single pass returns with cells unchanged when they have no passing face neighbor. -The NX implementation is a direct port. The refactor changed internal mechanics only — output is invariant to those changes for float32 arrays. +**Evidence:** All ten release snapshots from `upstream/release/v7.0.0` through `upstream/release/v7.4.2`, the current algorithm, and the DREAM.3D `v6.5.171` source repeat based on the count of failing cells. The current algorithm's API comment also identifies this nontermination condition. The synthetic surrounded-center section disables the loop and checks one pass; it does not test termination with no passing cells. 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. From 656d5ea1b7eeeaa887d980c7b240ed25751d007a Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 20:01:29 -0400 Subject: [PATCH 60/91] DOC: Rewrite the RequireMinNumNeighbors V&V record to follow the report template Signed-off-by: Michael Jackson --- .../vv/RequireMinNumNeighborsFilter.md | 265 ++++++------------ .../RequireMinNumNeighborsFilter.md | 38 ++- 2 files changed, 105 insertions(+), 198 deletions(-) diff --git a/src/Plugins/SimplnxCore/vv/RequireMinNumNeighborsFilter.md b/src/Plugins/SimplnxCore/vv/RequireMinNumNeighborsFilter.md index d5e34b24fd..a49ef53fe9 100644 --- a/src/Plugins/SimplnxCore/vv/RequireMinNumNeighborsFilter.md +++ b/src/Plugins/SimplnxCore/vv/RequireMinNumNeighborsFilter.md @@ -1,202 +1,112 @@ # V&V Report: RequireMinNumNeighborsFilter -| | | -|-----------|------------------| -| Plugin | SimplnxCore | +| | | +|--------|--------------| +| Plugin | SimplnxCore | | SIMPLNX UUID | `4ab5153f-6014-4e6d-bbd6-194068620389` | -| DREAM3D 6.5.171 equivalent | `MinNeighbors` (SIMPL UUID `dab5de3c-5f81-5bb5-8490-73521e1183ea`) - `Source/Plugins/Processing/ProcessingFilters/MinNeighbors.{h,cpp}` | +| SIMPLNX Human Name | Require Minimum Number of Neighbors | +| DREAM.3D v6.5.171 equivalent | `MinNeighbors` (Processing plugin, SIMPL UUID `dab5de3c-5f81-5bb5-8490-73521e1183ea`) | | Verified commit | ** | | Status | COMPLETE | -| Sign-off | Jared Duffey, 07-31-2026. Second engineer: Michael A. Jackson , 2026-08-07. | +| Sign-off | Jared Duffey, 2026-07-31; second engineer: Michael Jackson, 2026-08-07 | ## At a glance -| Aspect | Current state | -|------------------------|--------------------------| -| Algorithm Relationship | **Minor changes** - the NX port retains the legacy selection, coarsening, and feature-removal while incorporating bug fixes and minor code improvements. | -| Oracle (confirmed) | **Class 1 analytical + Class 4 invariants** - two mode-specific 4x1x1 inputs and one common 6x6x6 input check exact cell copying, phase selection, iterative coarsening, and feature compaction. | -| Code paths enumerated | 16 of 18 active paths are assertion-covered; cancellation and defensive error paths are not covered. | -| Tests today | 14 active test cases: two analytical oracle tests, four execute-error tests, negative-FeatureId reassignment, three preflight errors, two strict/shared-path tests, the Small IN100 regression, and SIMPL 6.4/6.5 conversion; two stale disabled tests are retired. | -| Exemplar archive | The inline oracle needs no output archive; retained input archive `6_5_test_data_1_v2.tar.gz` has SHA512 `585b51ba...3027d6c` and a provenance sidecar. | -| Legacy comparison | **Complete (2026-07-31)** - DREAM3D 6.5.171 and SIMPLNX were run on the 4x1x1 and 6x6x6 analytical fixtures in all-phases and single-phase modes; all 22 array comparisons and 2,646 exact values matched the independent expectations. | -| Bug flags | `RequireMinNumNeighborsFilter-D1`, `RequireMinNumNeighborsFilter-D2`, `RequireMinNumNeighborsFilter-D3` | -| V&V phase | **COMPLETE.** All V&V phases complete; second-engineer review signed off by Michael A. Jackson, 2026-08-07. | +| Aspect | Current state | +|--------|---------------| +| Algorithm Relationship | Minor changes to `MinNeighbors` in DREAM.3D v6.5.171. SIMPLNX retains threshold selection and face-neighbor voting, with input guards and bounded cell transfers. | +| Oracle (confirmed) | Class 1 analytical values and Class 4 invariants are encoded in two tests with three distinct inputs. All pass in CI. | +| Code paths enumerated | 20 of 30 paths exercised. Gaps include cancellation, store failures and defensive helper paths. | +| Tests today | 14 test cases cover both phase modes, exact cell copying, feature compaction, errors, shared paths, Small IN100 and SIMPL conversion. All pass in CI. | +| Exemplar archive | `6_5_test_data_1_v2.tar.gz` supplies Small IN100 input. The analytical expectations are encoded in the tests. | +| Legacy comparison | Not run — comparison artifacts are not available. | +| Bug flags | None | +| V&V phase | Analytical verification and second-engineer sign-off are complete. Legacy comparison artifacts are not available. | ## Summary -`RequireMinNumNeighborsFilter` removes features below a neighbor-count threshold, fills their cells from dominant valid face neighbors, and compacts feature-level arrays. Verification uses two independently derived analytical tests with Class 4 invariants, focused error tests, a Small IN100 regression, and SIMPL conversion checks. Both analytical fixture families matched DREAM3D 6.5.171 in all-phases and single-phase modes; three legacy bugs affecting negative or out-of-range FeatureIds and stalled coarsening are corrected in SIMPLNX and documented as D1-D3. +`RequireMinNumNeighborsFilter` removes features below a neighbor-count threshold, fills their cells from dominant valid face neighbors, and compacts feature arrays. Class 1 fixtures and Class 4 invariants check both phase modes, with regression and input-validation tests. All pass in CI; DREAM3D-NX v7.4.2 contains the three inherited defect fixes. ## Algorithm Relationship -*Classification:* Port | **Minor changes** | Rewrite | New filter - -*Evidence:* The NX algorithm retains the legacy `MinNeighbors` selection, voxel reassignment, and inactive-feature-removal. It replaces direct neighbor indexing with shared utilities and incorporates fixes for ignored arrays, data-array updates, invalid feature IDs, and non-terminating coarsening. During this V&V cycle, negative FeatureIds skip initial marking and enter the reassignment pass, out-of-range IDs return `-55567` before they are used for `activeObjects` indexing, and an iteration that cannot fill any remaining negative cell returns `-55572`. - -*PR(s):* - -- PR #154 ("Added MinNeighbors filter") - introduced the filter as `MinNeighbors` in `ComplexCore`. -- PR #183 ("Added ability to specify initial value of DataStore") - updated data-store initialization APIs. -- PR #226 ("Add optional parameters that skip validation when inactive") - made optional-parameter validation conditional. -- PR #266 ("Refactored geometry hierarchy") - adapted the filter to the geometry-hierarchy refactor. -- PR #272 ("Filter Parameter Organization and Code Cleanup") - reorganized parameters and cleaned filter code. -- PR #275 ("Filter and Unit Test Fixes") - applied general filter and test corrections. -- PR #299 ("Geometry: Update classes to reuse functionality from higher level classes and rename methods") - updated geometry API usage. -- PR #346 ("DataPathSelectionUpdates") - migrated data-path selection parameters. -- PR #349 ("DOCS: Update paths and CMake codes to prepare for documentation updates") - updated documentation and build-path references. -- PR #351 ("Add Filter Comments") - added filter comment support. -- PR #378 ("MultiArraySelectionParameter Updates") - updated multi-array selection parameter handling. -- PR #437 ("BUG: Fix filter default tags") - corrected default filter tags. -- PR #671 ("API: Add C++ Class Name to All Default Tags") - added the C++ class name to default tags. -- PR #684 ("ENH: Improve the Tuple Count validation error reporting") - improved tuple-count validation diagnostics. -- PR #735 ("ENH: Create DataModifiedAction that marks DataObjects as being modified by a filter") - adopted modified-data action tracking. -- PR #779 ("ENH: Implement SIMPL pipeline conversion") - added SIMPL pipeline conversion. -- PR #801 ("ENH: Rename complex to simplnx") - moved the filter from `ComplexCore` to `SimplnxCore`. -- PR #874 ("ENH: Refactor the Parameter Keys to make them consistent and easy to learn") - renamed parameter keys. -- PR #926 ("BUG: Filters that delete NeighborLists from the DataStructure send strong warning messages") - added NeighborList-removal warnings. -- PR #931 ("ENH: All filter's class names end with \"Filter\".") - renamed `MinNeighbors` to `MinNeighborsFilter`. -- PR #934 ("BUG: Pipeline and Filter human facing label cleanup") - corrected the filter's user-facing label. -- PR #956 ("ENH: Rename Filters that start with Find/Generate/Calculate to Compute") - renamed `MinNeighborsFilter` to `RequireMinNumNeighborsFilter`. -- PR #980 ("ENH: Update docs for filters that change FeatureIds to warn user of invalid feature attribute matrix") - added invalid feature-attribute-matrix guidance. -- PR #1017 ("ENH/BUG: Data Array to Store, Speed Optimizations, Code Cleanup (SimplnxCore)") - migrated array access to data stores and cleaned code. -- PR #1082 ("SIMPLConversion header optimization") - optimized SIMPL conversion includes. -- PR #1088 ("Added versioning to filter parameters and json") - added parameter and JSON versioning. -- PR #1238 ("ENH: Added pipeline relative path support") - added relative-path pipeline support. -- PR #1249 ("COMP: Misc. compiler warning cleanups.") - resolved compiler warnings. -- PR #1278 ("BUG: Ensure FeatureId arrays are range checked against the Feature Attribute Matrix.") - added FeatureId range validation. -- PR #1310 ("BUG: Fix RequireMinNumNeighbors Not Using Ignore Paths") - corrected ignored voxel-array handling and introduced the separate algorithm class. -- PR #1320 ("BUG: RequireMinNumNeighbors DataArray Update fixes") - corrected cell-data updates during reassignment. -- PR #1343 ("BUG: Fix out-of-bounds array access in RequireMinNumNeighbors Filter") - added invalid FeatureId error handling. -- PR #1377 ("STY: Ensure all code arguments are consistent across filters") - standardized filter argument style. -- PR #1439 ("ENH/API: Multi-Dimensional Tuple Support for StringArray and NeighborList") - adapted to multidimensional NeighborList APIs. -- PR #1523 ("ENH: Factor out the 6-face neighbor code that is systemic through out the code base") - extracted shared six-face neighbor utilities. -- PR #1535 ("ENH: Remove redundant preflight checks that are already done in the parameter") - removed duplicated preflight checks. -- PR #1590 ("ENH: Standardize 2D Image Handling") - standardized shared image-neighbor behavior. -- PR #1682 ("BUG: Fix SIMPL pipeline conversion bugs and Write Image naming") - corrected optional SIMPL conversion argument handling. +*Classification:* Minor changes -## Oracle - -*Class:* **1 (Analytical)** primary + **4 (Invariant)** companion. - -Expected values are derived from the fixture definitions and the documented face-neighbor traversal, without using output from SIMPLNX or DREAM3D 6.5.171. - -### Applied: 4x1x1 analytical variants - -Both variants use: - -- Geometry dimensions `4x1x1`. -- Minimum-neighbor threshold `2`, selected phase `1`, and four feature tuples numbered 0 through 3. -- Cell `FeatureIds = [2, 1, 1, 3]`. -- Copied cell values `[20, 101, 102, 30]`. -- Ignored cell values `[200, 101, 102, 300]`. -- Feature `Phases = [0, 1, 2, 1]`. - -Feature 1 owns the two middle cells and is rejected in both variants. The left rejected cell has feature 2 as its only retained face neighbor, and the right rejected cell has feature 3 as its only retained face neighbor. Tuple copying therefore produces `[20, 20, 30, 30]`. Removing input feature 1 compacts input feature 2 to output ID 1 and input feature 3 to output ID 2, producing final `FeatureIds = [1, 1, 2, 2]`. The ignored array is unchanged. - -The variants intentionally use different `NumNeighbors` inputs: - -| Variant | `ApplyToSinglePhase` | Input `NumNeighbors` | Selection derivation | Expected compacted `NumNeighbors` | Expected compacted `Phases` | -|---|---:|---|---|---|---| -| All-phase | `false` | `[0, 0, 3, 3]` | Feature 1 is below 2; features 2 and 3 meet the threshold. | `[0, 3, 3]` | `[0, 2, 1]` | -| Single-phase | `true` | `[0, 0, 0, 3]` | Feature 1 is selected phase 1 and below 2; feature 2 remains active because it is phase 2 even though its count is 0; feature 3 meets the threshold. | `[0, 0, 3]` | `[0, 2, 1]` | - -These are two distinct analytical inputs, not one input with only the phase-mode argument changed. - -### Applied: discriminating 6x6x6 analytical fixture - -Coordinates use `0 <= x,y,z <= 5` and linear cell index `i = z * 36 + y * 6 + x`. The fixture uses the same input in both phase modes: - -- Every cell starts as feature 2. -- Feature 1 replaces the `3x3x3` cube `1 <= x,y,z <= 3` and the four cells `(5,5,5)`, `(4,5,5)`, `(0,5,5)`, and `(5,0,5)`. Feature 1 therefore owns 31 rejected cells. -- Feature 3 owns seed `(5,5,4)`. -- Feature 4 owns seed `(5,4,5)`. -- Feature 5 owns no cells. -- Feature `NumNeighbors = [0, 0, 3, 4, 5, 0]`. -- Feature `Phases = [0, 1, 1, 1, 1, 2]`. -- Minimum-neighbor threshold is `3`; selected phase is `1`. -- `CopiedScalar[i] = 10000 + i`. -- `CopiedVector[i] = [i + 0.25, i + 0.5, i + 0.75]` as `float32[3]`. -- `IgnoredValues[i] = 20000 + i`. +*Evidence:* `src/Plugins/SimplnxCore/src/SimplnxCore/SimplnxCoreLegacyUUIDMapping.hpp` maps `MinNeighbors` to this filter; both use threshold selection and six-face voting, while SIMPLNX adds safety guards and compacts cell IDs before filling. -Feature 1 is rejected in both modes. The 26 cube-surface cells and four additional rejected cells can copy from retained face neighbors during the first coarsening pass; only cube center `(2,2,2)` remains for the second pass. The analytically derived rejected-set sizes are therefore 31, then 1, then 0. - -The face-neighbor traversal is `-Z, -Y, -X, +X, +Y, +Z`. The source changes only when a feature's vote count is strictly greater than the prior maximum: - -- Tie cell `(5,5,5)` sees feature 3 at `-Z` and feature 4 at `-Y`. The 1-vs-1 tie retains the first vote, so it copies from feature-3 seed `(5,5,4)`. -- Additional rejected cells `(4,5,5)`, `(0,5,5)`, and `(5,0,5)` copy from `(3,5,5)`, `(1,5,5)`, and `(5,1,5)`, respectively. -- Cube center `(2,2,2)` chooses its `+Z` neighbor during the second pass. That neighbor copied from exterior cell `(2,2,4)` during the first pass, so the center's final scalar and vector values originate at `(2,2,4)`. -- For every other rejected cube cell, the original source is selected by the following first-matching rule: if `z == 3`, use `(x,y,4)`; else if `y == 3`, use `(x,4,z)`; else if `x == 3`, use `(4,y,z)`; else if `x == 1`, use `(0,y,z)`; else if `y == 1`, use `(x,0,z)`; otherwise use `(x,y,0)`. This is the closed-form consequence of the traversal order when every exterior vote is for feature 2. -- Every nonrejected cell uses itself as its source. - -After feature compaction, the feature-3 seed and tie cell have output ID 2, the feature-4 seed has output ID 3, and every other cell has output ID 1. The copied scalar and three vector components equal their formulas evaluated at the source cell above. `IgnoredValues` remains `20000 + i`. +## Oracle -| Mode | Removed input features | Retained input-to-output mapping | Expected compacted `NumNeighbors` | Expected compacted `Phases` | -|---|---|---|---|---| -| All-phase | 1 and zero-cell feature 5 | `2 -> 1`, `3 -> 2`, `4 -> 3` | `[0, 3, 4, 5]` | `[0, 1, 1, 1]` | -| Single-phase | 1 only; feature 5 remains because it is phase 2 | `2 -> 1`, `3 -> 2`, `4 -> 3`, `5 -> 4` | `[0, 3, 4, 5, 0]` | `[0, 1, 1, 1, 2]` | +*Class:* 1 (Analytical), with 4 (Invariant). -Because feature 5 owns no cells, both modes have identical final cell arrays even though their compacted feature arrays have different tuple counts. +*Applied:* Derive exact copied values and compacted feature IDs from three input fixtures; check ignored values, ID bounds and inherited tuple dimensions. -### Applied: Class 4 invariants +*Encoded:* `src/Plugins/SimplnxCore/test/RequireMinNumNeighborsTest.cpp::SimplnxCore::RequireMinNumNeighborsFilter: Analytical Oracle` and `SimplnxCore::RequireMinNumNeighborsFilter: Discriminating 6x6x6 Analytical Fixture` — two mode-specific 4×1×1 inputs and one 6×6×6 input in both modes. All pass in CI. -- Every final `FeatureIds` value is nonnegative, in range, and contiguous after compaction. -- Ignored cell values are unchanged. -- Both 6x6x6 phase modes produce identical final cell `FeatureIds`. -- Every cell and feature array continues to inherit its parent Attribute Matrix tuple dimensions. +*Second-engineer review:* Michael Jackson, 2026-08-07. -### Encoded +## Bugs found and fixed -- `test/RequireMinNumNeighborsTest.cpp::"SimplnxCore::RequireMinNumNeighborsFilter: Analytical Oracle"` - two mode-specific 4x1x1 inputs; each asserts 18 exact array elements plus eight final-ID invariant predicates. -- `test/RequireMinNumNeighborsTest.cpp::"SimplnxCore::RequireMinNumNeighborsFilter: Discriminating 6x6x6 Analytical Fixture"` - one common input executed in both phase modes; asserts 2,610 exact cell and feature values across six arrays, identical final cell `FeatureIds`, and inherited tuple dimensions. -- Both test cases pass in the current in-core Debug build. +All defects in this table are fixed. DREAM3D-NX v7.4.2 contains the fixes for `RequireMinNumNeighborsFilter-D1`, `RequireMinNumNeighborsFilter-D2` and `RequireMinNumNeighborsFilter-D3`. -*Second-engineer review:* **Signed off by Michael A. Jackson, 2026-08-07.** The V&V work was authored by Jared Duffey (PR #1694), so the second-engineer review is independent of the author. +| Deviation | Defect | Affected released versions | Resolution | +|-----------|--------|----------------------------|------------| +| `RequireMinNumNeighborsFilter-D1` | Negative FeatureIds caused an out-of-bounds feature-state lookup. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | Negative IDs bypass the lookup and enter face-neighbor reassignment. | +| `RequireMinNumNeighborsFilter-D2` | A FeatureId at or above the feature tuple count reached an unchecked feature-state lookup. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | The marking pass returns `-55567` before the invalid lookup. | +| `RequireMinNumNeighborsFilter-D3` | Coarsening could loop indefinitely when unresolved cells had no valid face neighbor. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was affected. | A scan with unresolved cells and no fill candidate returns `-55572`. | ## Code path coverage -16 of 18 active paths are assertion-covered. Cancellation and defensive infrastructure/error paths are documented gaps. +20 of 30 paths exercised. Each uncovered path is listed below. -Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/RequireMinNumNeighbors.cpp`, shared reassignment in `Algorithms/FillBadVoxels.cpp`, and preflight validation in `Filters/RequireMinNumNeighborsFilter.cpp`. +Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/RequireMinNumNeighbors.cpp` (228 lines); `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/FillBadVoxels.cpp` (469 lines); `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/RequireMinNumNeighborsFilter.cpp` (237 lines). -Rows 4, 15, and 16 are retired and excluded from the active-path count; their former checks are absent from the current implementation. - -The filter selects features to retain, marks rejected-feature voxels, iteratively copies a dominant face neighbor into each rejected voxel, and removes inactive feature tuples. +Test names below omit the common `SimplnxCore::RequireMinNumNeighborsFilter: ` prefix. `Small IN100 regression` means `SimplnxCore::RequireMinNumNeighborsFilter`. | # | Phase | Path | Test case | -|---|---|---|---| -| 1 | Preflight | Feature-level `NumNeighbors` and, when enabled, `FeaturePhases` tuple counts disagree - return `-252`. | `SimplnxCore::RequireMinNumNeighborsFilter: Preflight Error - tuple count mismatch (-252)` | -| 2 | Preflight | FeatureIds is not in the selected Image Geometry's Cell Attribute Matrix - return `-55575`. | `SimplnxCore::RequireMinNumNeighborsFilter: Preflight Error - FeatureIds outside Cell Attribute Matrix (-55575)` | -| 3 | Preflight | NeighborList-removal preflight warning is produced when feature NeighborLists are present. | `SimplnxCore::RequireMinNumNeighborsFilter` - asserts only one `-5558` warning. | -| 4 | Retired | The former cell-data discovery error check is absent. `FillBadVoxels` uses `GenerateDataArrayList` without this error return. | Not an active path. | -| 5 | Selection | Single-phase mode names a phase absent from `FeaturePhases` - return `-5555`. | `SimplnxCore::RequireMinNumNeighborsFilter: Execute Error - unavailable phase (-5555)` | -| 6 | Selection | Every eligible feature is below the neighbor threshold - return `-55569` before mutation. | `SimplnxCore::RequireMinNumNeighborsFilter: Execute Error - all features rejected (-55569)` | -| 7 | Selection | All-phase or selected-phase feature meets threshold; nonselected phases remain active, including a zero-cell feature below the threshold. | `SimplnxCore::RequireMinNumNeighborsFilter: Analytical Oracle` (`ApplyToSinglePhase=0/1`) and `SimplnxCore::RequireMinNumNeighborsFilter: Discriminating 6x6x6 Analytical Fixture` | -| 8 | Cancellation | Cancellation before mutation or within coarsening returns without further changes. | *Not directly tested. Requires cancel signal injection.* | -| 9 | Marking | Rejected feature IDs are marked and retained feature IDs remain available for coarsening. | Both analytical oracle tests assert the exact post-marking/coarsening result. | -| 10 | Reassignment | Rejected cells honor X/Y/Z boundary validity and `-Z,-Y,-X,+X,+Y,+Z` traversal, preserve the first feature in a 1-vs-1 tie, and repeat until an enclosed rejected cell can be filled. | `SimplnxCore::RequireMinNumNeighborsFilter: Discriminating 6x6x6 Analytical Fixture` - exact source-index assertions cover all axes, the tie, boundary cells, and the second-pass cube center. | -| 11 | Reassignment | Feature ID is outside the feature-array range - return `-55567` before it is used for indexing. | `SimplnxCore::RequireMinNumNeighborsFilter: Execute Error - feature ID out of range (-55567)` | -| 12 | Reassignment | Negative Feature IDs skip initial marking and are reassigned from valid face neighbors. | `SimplnxCore::RequireMinNumNeighborsFilter: Execute - negative feature ID is reassigned` | -| 13 | Reassignment | Negative Feature IDs remain but an iteration finds no non-negative face neighbor to copy - return `-55572` instead of looping indefinitely. | `SimplnxCore::RequireMinNumNeighborsFilter: Execute Error - no coarsening progress (-55572)` | -| 14 | Copy | A rejected cell copies every nonignored cell-data tuple from its selected neighbor while ignored arrays remain unchanged. | Both analytical oracle tests; the 6x6x6 fixture checks scalar `int32`, three-component `float32`, ignored `int32`, and `FeatureIds` tuples for all 216 cells in both modes. | -| 15 | Retired | The former destination tuple-index check is absent from the current slice-based copy implementation. | Not an active path. | -| 16 | Retired | No explicit source tuple-index range error exists in the current slice-based copy implementation. Face-neighbor selection constrains source indexes to valid cells. | Not an active path. Boundary copying is covered by `SimplnxCore::RequireMinNumNeighborsFilter: Discriminating 6x6x6 Analytical Fixture`. | -| 17 | Finalize | Inactive feature tuples are removed and remaining FeatureIds are remapped; NeighborLists are removed. | `SimplnxCore::RequireMinNumNeighborsFilter`, both analytical oracle tests, and their exact compacted `NumNeighbors`, `Phases`, and `FeatureIds` assertions. | -| 18 | Finalize | Inactive-feature removal fails - propagate the error returned by `RemoveInactiveObjects`. | *Not directly tested. Defensive check.* | -| 19 | Preflight | FeatureIds tuple count differs from the selected Image Geometry cell count - return `-59571`. | `SimplnxCore::RequireMinNumNeighborsFilter: Selected cell tuple counts` | -| 20 | Preflight | A Cell Arrays to Ignore path is not a direct child of the selected geometry's Cell Attribute Matrix - return `-55573`. Membership uses the parent object's ID, so another path to the same matrix is accepted. | `SimplnxCore::RequireMinNumNeighborsFilter: Strict Feature Ids` - `Ignored array in a second Attribute Matrix`; shared-path acceptance is covered by `SimplnxCore::RequireMinNumNeighborsFilter: Shared Cell Paths and Selected Feature Matrix`. | -| 21 | Preflight | The parent of Number of Neighbors is not an Attribute Matrix directly under the selected geometry, or is its Cell Attribute Matrix - return `-55574`. | `SimplnxCore::RequireMinNumNeighborsFilter: Strict Feature Ids` - `Feature array outside the selected geometry`, `Feature array under a plain DataGroup`, and `Feature array in the Cell Attribute Matrix`. | +|---|-------|------|-----------| +| 1 | Preflight | Feature array tuple counts differ → `-252`. | `Preflight Error - tuple count mismatch (-252)` | +| 2 | Preflight | FeatureIds is outside the geometry's Cell Attribute Matrix → `-55575`. | `Preflight Error - FeatureIds outside Cell Attribute Matrix (-55575)`; `Strict Feature Ids` | +| 3 | Preflight | FeatureIds tuple count differs from the geometry cell count → `-55571`. | `Preflight Error - FeatureIds tuple count mismatch (-55571)` | +| 4 | Preflight | An ignored array is outside the Cell Attribute Matrix → `-55573`. | `Strict Feature Ids` — `Ignored array in a second Attribute Matrix` | +| 5 | Preflight | Number of Neighbors has an invalid Feature Attribute Matrix parent → `-55574`. | `Strict Feature Ids` — external, plain-group and Cell Attribute Matrix parents | +| 6 | Paths | Resolve shared Cell paths and compact the selected Feature Attribute Matrix. | `Shared Cell Paths and Selected Feature Matrix` — both alias variants | +| 7 | Preflight | Warn that NeighborLists will be removed. | Small IN100 regression — checks one warning, `-5558` | +| 8 | Selection | Requested phase is absent → `-5555`. | `Execute Error - unavailable phase (-5555)` | +| 9 | Selection | No nonbackground feature survives → `-55569`. | `Execute Error - all features rejected (-55569)` | +| 10 | Selection | All-phase mode retains features at or above the threshold. | `Analytical Oracle` — `ApplyToSinglePhase=0`; `Discriminating 6x6x6 Analytical Fixture` | +| 11 | Selection | Single-phase mode also retains other phases, including a feature with no cells. | `Analytical Oracle` — `ApplyToSinglePhase=1`; `Discriminating 6x6x6 Analytical Fixture` | +| 12 | Marking | Negative IDs bypass feature-state indexing and enter reassignment. | `Execute - negative feature ID is reassigned` | +| 13 | Marking | Nonnegative ID exceeds the feature range → `-55567`. | `Execute Error - feature ID out of range (-55567)` | +| 14 | Marking | Mark rejected cells and renumber retained IDs in full and partial 65,536-cell transfers. | Small IN100 regression — million-cell input; analytical fixtures check exact remapped IDs | +| 15 | Voting | Clip all six faces and preserve the first winning feature in a tie. | `Discriminating 6x6x6 Analytical Fixture` — boundary and source-index assertions | +| 16 | Filling | Repeat propagation to fill an enclosed rejected cell. | `Discriminating 6x6x6 Analytical Fixture` — cube center uses the second-pass source | +| 17 | Copy | Copy scalar and vector tuples while preserving ignored arrays. | `Analytical Oracle`; `Discriminating 6x6x6 Analytical Fixture` | +| 18 | Finalize | Compact feature arrays with the same mapping used for cell IDs. | Both analytical tests; `Shared Cell Paths and Selected Feature Matrix` | +| 19 | Filling | Unresolved cells have no fill candidate → `-55572`. | `Execute Error - no coarsening progress (-55572)` | +| 20 | Filling | No negative IDs remain → finish without another copy pass. | `Execute - negative feature ID is reassigned`; both analytical tests | +| 21 | Setup | A geometry dimension is zero → skip filling. | *Not directly tested. No fixture has a zero-sized geometry.* | +| 22 | Cancellation | Stop during selection, marking, filling or compaction. | *Not directly tested. No fixture injects cancellation.* | +| 23 | Validation | The shared fill scan finds an out-of-range positive ID → `-55567`. | *Not directly tested. The marking pass rejects this input before the shared scan.* | +| 24 | I/O | Marking reads or writes fail → propagate the store error. | *Not directly tested. No fixture injects a marking-store failure.* | +| 25 | I/O | Slice reads or writes fail during scanning, tuple copying or ID filling → propagate the error. | *Not directly tested. No fixture injects a slice-store failure.* | +| 26 | Finalize | Inactive-feature removal fails → propagate its error. | *Not directly tested. No fixture forces the compaction helper to fail.* | +| 27 | Filling | FeatureIds is selected as ignored but is still filled last. | *Not directly tested. No fixture includes FeatureIds in the ignored selection.* | +| 28 | Copy | Dispatch cell-array types other than the analytical int32 and float32 types. | *Not directly tested. No independent fixture checks exact copied values for each remaining type.* | +| 29 | Preflight | Report stale feature arrays in updated preflight values. | *Not directly tested. Tests do not assert the updated-value text.* | +| 30 | Finalize | Delete feature NeighborLists. | *Not directly tested. The regression checks the warning but does not assert that the arrays are absent.* | ## Test inventory +All 14 test cases are in `src/Plugins/SimplnxCore/test/RequireMinNumNeighborsTest.cpp`. All pass in CI. Dynamic sections are listed separately below. + | Test case | Status | Notes | |---|---|---| | `SimplnxCore::RequireMinNumNeighborsFilter: Analytical Oracle` | new-for-V&V | Class 1/4 harness for two generated 4x1x1 variants. Each variant checks five arrays, 18 exact element values, eight final-ID invariants, feature-array tuple counts, and inherited tuple dimensions. | | `ApplyToSinglePhase=0` | new-for-V&V | `DYNAMIC_SECTION` generated by `GENERATE(false, true)`; uses input `NumNeighbors [0,0,3,3]` and verifies all-phase selection and compaction. | | `ApplyToSinglePhase=1` | new-for-V&V | `DYNAMIC_SECTION` generated by `GENERATE(false, true)`; uses input `NumNeighbors [0,0,0,3]` and verifies that nonselected phase-2 feature 2 remains active. | | `SimplnxCore::RequireMinNumNeighborsFilter: Discriminating 6x6x6 Analytical Fixture` | new-for-V&V | Runs one common input in both phase modes. Checks six arrays with 2,610 exact element comparisons, a 1-vs-1 traversal-order tie, X/Y/Z boundaries, second-pass copying, ignored values, zero-cell feature retention, equal cell output across modes, and tuple dimensions. | -| `SimplnxCore::RequireMinNumNeighborsFilter` | kept | Modified: loads `6_5_test_data_1_v2.tar.gz`, recomputes neighbors, derives the expected retained `NumElements` sequence from the pre-filter input and threshold, checks all 791 retained values, asserts the single `-5558` preflight warning, and verifies tuple dimensions. | +| `SimplnxCore::RequireMinNumNeighborsFilter` | kept | Modified: loads `6_5_test_data_1_v2.tar.gz`, recomputes neighbors, derives the expected retained `NumElements` sequence from the pre-filter input and threshold, checks every retained value, asserts the single `-5558` preflight warning, and verifies tuple dimensions. | | `SimplnxCore::RequireMinNumNeighborsFilter: Preflight Error - tuple count mismatch (-252)` | kept | Creates 5-tuple `NumNeighbors` and 4-tuple `Phases` arrays, then asserts invalid preflight and exact code `-252`. | | `SimplnxCore::RequireMinNumNeighborsFilter: Preflight Error - FeatureIds outside Cell Attribute Matrix (-55575)` | new-for-V&V | Places `FeatureIds` in a second Attribute Matrix with the same tuple count as the Cell Attribute Matrix, then asserts invalid preflight and exact code `-55575`. | -| `SimplnxCore::RequireMinNumNeighborsFilter: Selected cell tuple counts` | new-for-V&V | Creates a 4-cell geometry with 3, 4, or 5 `FeatureIds` tuples stored separately. Equal counts pass; mismatches assert invalid preflight, exact code `-59571`, selected paths, tuple count, and cell count. | | `SimplnxCore::RequireMinNumNeighborsFilter: Execute Error - feature ID out of range (-55567)` | new-for-V&V | Supplies `FeatureIds [1,4,2,3]` for four feature tuples, asserts valid preflight, invalid execution, and exact code `-55567`; covers D2. | | `SimplnxCore::RequireMinNumNeighborsFilter: Execute - negative feature ID is reassigned` | new-for-V&V | Supplies `FeatureIds [-1,1,1,2]`, asserts successful execution, and checks all four final IDs equal `[1,1,1,2]`; covers D1. | | `SimplnxCore::RequireMinNumNeighborsFilter: Execute Error - unavailable phase (-5555)` | new-for-V&V | Selects absent phase 5, then asserts valid preflight, invalid execution, and exact code `-5555`. | @@ -205,39 +115,22 @@ The filter selects features to retain, marks rejected-feature voxels, iterativel | `SimplnxCore::RequireMinNumNeighborsFilter: SIMPL Backwards Compatibility` | kept | Loads the two conversion fixtures below and verifies one converted filter, UUID, comments, and seven converted arguments in each section. | | `SIMPL 6.5 (UUID)` | kept | `DYNAMIC_SECTION` using `test/simpl_conversion/6_5/RequireMinNumNeighborsFilter.json`; verifies UUID-based 6.5 pipeline conversion. | | `SIMPL 6.4 (Filter_Name)` | kept | `DYNAMIC_SECTION` using `test/simpl_conversion/6_4/RequireMinNumNeighborsFilter.json`; verifies name-based 6.4 pipeline conversion. | -| `SimplnxCore::RequireMinNumNeighborsFilter: Bad Phase Number` | retired | Removed from `#if 0`; it used stale parameter keys and could not compile. The active `unavailable phase (-5555)` test replaces its intended error coverage. | -| `SimplnxCore::RequireMinNumNeighborsFilter: Phase Array` | retired | Removed from `#if 0`; it used stale parameter keys and could not compile. Current analytical variants cover the all-phase and single-phase selection behavior. | +| `SimplnxCore::RequireMinNumNeighborsFilter: Preflight Error - FeatureIds tuple count mismatch (-55571)` | new-for-V&V | Creates a 5-cell geometry with four `FeatureIds` tuples in its Cell Attribute Matrix, then asserts invalid preflight and exact code `-55571`. | | `SimplnxCore::RequireMinNumNeighborsFilter: Strict Feature Ids` | new-for-V&V | Checks valid Cell and Feature Attribute Matrices, shared Cell paths, and read-only phases in another matrix. Rejects misplaced Feature Ids (`-55575`), ignored arrays (`-55573`), and invalid Feature Attribute Matrix locations (`-55574`); checks each error code and affected paths. | | `SimplnxCore::RequireMinNumNeighborsFilter: Shared Cell Paths and Selected Feature Matrix` | new-for-V&V | Alternates geometry and shared-parent paths for Feature Ids and ignored arrays. Checks geometry-based modification paths, reassigned IDs and cell values, unchanged ignored values, selected Feature Attribute Matrix compaction, unchanged external marker values, and inherited tuple dimensions. | -There are 14 active `TEST_CASE`s: 3 kept and 11 new for this V&V cycle. - ## Exemplar archive -No new output archive was created for this V&V cycle because both Class 1 fixtures and their expected values are encoded inline. - -The retained Small IN100 regression uses this shared input archive: - -| Field | Value | -|---|---| -| Archive | `6_5_test_data_1_v2.tar.gz` | -| SHA512 | `585b51ba1da9784a204fe88073ca562b45afd7007cf451b0193079b885c4b4caff7cf21b13e016433b84155546ac0f73f003a8b8ebb1c58360b2c56de3027d6c` | -| CMake registration | `src/Plugins/SimplnxCore/test/CMakeLists.txt` | -| Provenance | `src/Plugins/SimplnxCore/vv/provenance/6_5_test_data_1_v2.md` | -| Role in this V&V | Input only. The test derives expected retained `NumElements` values from pre-filter arrays and the independently recomputed neighbor threshold. | - -No archived array is used as the canonical correctness oracle for this filter. - -## Deviations from DREAM3D 6.5.171 - -On 2026-07-31, DREAM3D 6.5.171 and SIMPLNX were run on the 4x1x1 and discriminating 6x6x6 analytical fixtures in both all-phases and single-phase modes. All 22 array comparisons and 2,646 exact values matched between the two implementations and the independent analytical expectations. - -The complete pipelines, shared inputs, outputs, logs, comparison tables, and checksums were uploaded to OneDrive on 2026-07-31 in `FilterVerification_RequireMinNumNeighborsFilter`. These valid fixtures do not trigger the D1-D3 error-condition deviations documented below. +- **Archive:** `6_5_test_data_1_v2.tar.gz` +- **SHA512:** `585b51ba1da9784a204fe88073ca562b45afd7007cf451b0193079b885c4b4caff7cf21b13e016433b84155546ac0f73f003a8b8ebb1c58360b2c56de3027d6c` +- **Provenance:** `src/Plugins/SimplnxCore/vv/provenance/6_5_test_data_1_v2.md` -Three user-visible differences occur for negative or out-of-range FeatureIds or a coarsening pass that cannot make progress: +## Deviations from DREAM.3D v6.5.171 -- `RequireMinNumNeighborsFilter-D1` - SIMPLNX safely accepts a negative FeatureId and reassigns it from a valid face neighbor; DREAM3D 6.5.171 uses the negative value as an out-of-bounds `activeObjects` index. -- `RequireMinNumNeighborsFilter-D2` - SIMPLNX returns `-55567` for an out-of-range non-negative FeatureId before indexing; DREAM3D 6.5.171 uses the value as an out-of-bounds `activeObjects` index. -- `RequireMinNumNeighborsFilter-D3` - SIMPLNX returns `-55572` when remaining negative cells have no non-negative face neighbor; DREAM3D 6.5.171 continues the coarsening loop indefinitely. +See `vv/deviations/RequireMinNumNeighborsFilter.md` for the root cause, affected users and recommendation. Source comparison establishes these Part 1 differences. -See `vv/deviations/RequireMinNumNeighborsFilter.md` for root cause, affected users, and migration recommendations. +| Deviation | Observed difference | +|-----------|---------------------| +| `RequireMinNumNeighborsFilter-D1` | SIMPLNX reassigns negative FeatureIds safely; DREAM.3D v6.5.171 uses them as unchecked feature-state indices. | +| `RequireMinNumNeighborsFilter-D2` | SIMPLNX rejects out-of-range nonnegative FeatureIds with `-55567`; DREAM.3D v6.5.171 uses them as unchecked indices. | +| `RequireMinNumNeighborsFilter-D3` | SIMPLNX returns `-55572` when coarsening cannot progress; DREAM.3D v6.5.171 continues the loop. | diff --git a/src/Plugins/SimplnxCore/vv/deviations/RequireMinNumNeighborsFilter.md b/src/Plugins/SimplnxCore/vv/deviations/RequireMinNumNeighborsFilter.md index fdd21ab69c..7f9e95c8e3 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/RequireMinNumNeighborsFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/RequireMinNumNeighborsFilter.md @@ -1,59 +1,73 @@ -# Deviations from DREAM3D 6.5.171: RequireMinNumNeighborsFilter +# Deviations from DREAM.3D v6.5.171: RequireMinNumNeighborsFilter -This file lists every documented behavioral difference between SIMPLNX `RequireMinNumNeighborsFilter` and DREAM3D 6.5.171 `MinNeighbors`. +Part 1 lists the three source-confirmed differences from `MinNeighbors` in DREAM.3D v6.5.171. The same defects affected SIMPLNX. DREAM3D-NX v7.4.2 contains all three fixes. -The valid all-phase analytical fixture produced matching FeatureIds, copied and ignored cell arrays, NumNeighbors, and Phases. The deviations below concern negative or out-of-range FeatureIds and a coarsening state that cannot make progress. +Entries use the stable `RequireMinNumNeighborsFilter-D` sequence. The Filter UUID is the permanent cross-reference anchor. --- -## RequireMinNumNeighborsFilter-D1 +## Part 1 — Differences from DREAM.3D v6.5.171 + +### RequireMinNumNeighborsFilter-D1 | Field | Value | |---|---| | **Deviation ID** | `RequireMinNumNeighborsFilter-D1` | | **Filter UUID** | `4ab5153f-6014-4e6d-bbd6-194068620389` | | **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:** When a cell has a negative FeatureId and a valid non-negative face neighbor, SIMPLNX reassigns the cell and completes; DREAM3D 6.5.171 performs an out-of-bounds read while marking cells for removal. +**Symptom:** When a cell has a negative FeatureId and a valid non-negative face neighbor, SIMPLNX reassigns the cell and completes; DREAM.3D v6.5.171 performs an out-of-bounds read while marking cells for removal. -**Root cause:** `bug` in DREAM3D 6.5.171. `MinNeighbors::merge_containedfeatures()` uses each FeatureId directly as an index into `activeObjects`, including negative values. SIMPLNX checks `featureId < 0`, leaves that cell for the coarsening pass, and copies from the dominant valid face neighbor. +**Root cause:** `bug` in DREAM.3D v6.5.171. `MinNeighbors::merge_containedfeatures()` uses each FeatureId directly as an index into `activeObjects`, including negative values. SIMPLNX guards the lookup with a nonnegative-ID check, marks the cell as `-1`, and copies from the dominant valid face neighbor. **Affected users:** Users whose input FeatureIds array already contains negative sentinel values. Inputs containing only valid, non-negative, in-range FeatureIds are unaffected. **Recommendation:** `trust SIMPLNX`. It handles the sentinel value without an invalid memory access and deterministically reassigns the cell when a valid face neighbor exists. +**Evidence:** `src/Plugins/SimplnxCore/test/RequireMinNumNeighborsTest.cpp::Execute - negative feature ID is reassigned` checks this correction. Source at each release from `upstream/release/v7.0.0` through `upstream/release/v7.4.1` contains the defect; `upstream/release/v7.4.2` contains the fix from PR #1694 (`0c8fa9f59`). DREAM.3D `v6.5.171`, `Source/Plugins/Processing/ProcessingFilters/MinNeighbors.cpp`, contains the defective path. + --- -## RequireMinNumNeighborsFilter-D2 +### RequireMinNumNeighborsFilter-D2 | Field | Value | |---|---| | **Deviation ID** | `RequireMinNumNeighborsFilter-D2` | | **Filter UUID** | `4ab5153f-6014-4e6d-bbd6-194068620389` | | **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:** When a non-negative FeatureId is greater than or equal to the number of feature tuples, SIMPLNX returns error `-55567`; DREAM3D 6.5.171 uses the value as an out-of-bounds `activeObjects` index. +**Symptom:** When a non-negative FeatureId is greater than or equal to the number of feature tuples, SIMPLNX returns error `-55567`; DREAM.3D v6.5.171 uses the value as an out-of-bounds `activeObjects` index. -**Root cause:** `bug` in DREAM3D 6.5.171. The legacy marking pass does not validate FeatureIds before indexing its feature-state vector. SIMPLNX validates the value before the first `activeObjects` lookup and stops with a deterministic error. +**Root cause:** `bug` in DREAM.3D v6.5.171. The legacy marking pass does not validate FeatureIds before indexing its feature-state vector. SIMPLNX validates the value before the first `activeObjects` lookup and stops with a deterministic error. **Affected users:** Users with malformed FeatureIds arrays or FeatureIds that do not correspond to the supplied feature Attribute Matrix. Valid in-range FeatureIds are unaffected. **Recommendation:** `trust SIMPLNX`. The explicit `-55567` failure prevents undefined behavior and identifies the invalid value and array. +**Evidence:** `src/Plugins/SimplnxCore/test/RequireMinNumNeighborsTest.cpp::Execute Error - feature ID out of range (-55567)` checks this correction. Source at each release from `upstream/release/v7.0.0` through `upstream/release/v7.4.1` contains the defect; `upstream/release/v7.4.2` contains the fix from PR #1694 (`0c8fa9f59`). DREAM.3D `v6.5.171`, `Source/Plugins/Processing/ProcessingFilters/MinNeighbors.cpp`, contains the defective path. + --- -## RequireMinNumNeighborsFilter-D3 +### RequireMinNumNeighborsFilter-D3 | Field | Value | |---|---| | **Deviation ID** | `RequireMinNumNeighborsFilter-D3` | | **Filter UUID** | `4ab5153f-6014-4e6d-bbd6-194068620389` | | **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:** When negative cells remain and none has a non-negative face neighbor, SIMPLNX returns error `-55572`; DREAM3D 6.5.171 continues the coarsening loop indefinitely. +**Symptom:** When negative cells remain and none has a non-negative face neighbor, SIMPLNX returns error `-55572`; DREAM.3D v6.5.171 continues the coarsening loop indefinitely. -**Root cause:** `bug` in DREAM3D 6.5.171. The legacy loop assumes every rejected cell will eventually reach a retained feature and does not detect an iteration that fills no cells. SIMPLNX tracks whether the current iteration found any fill candidate and returns `-55572` when unresolved cells remain without progress. +**Root cause:** `bug` in DREAM.3D v6.5.171. The legacy loop assumes every rejected cell will eventually reach a retained feature and does not detect an iteration that fills no cells. SIMPLNX counts the current scan's fill candidates and returns `-55572` when unresolved cells remain without progress. **Affected users:** Users with disconnected negative regions that have no retained-feature seed, including an all-negative FeatureIds array paired with a feature table that still contains an active feature. **Recommendation:** `trust SIMPLNX`. The deterministic error prevents an unbounded execution and tells the user how to correct the input. + +**Evidence:** `src/Plugins/SimplnxCore/test/RequireMinNumNeighborsTest.cpp::Execute Error - no coarsening progress (-55572)` checks this correction. Source at each release from `upstream/release/v7.0.0` through `upstream/release/v7.4.1` contains the defect; `upstream/release/v7.4.2` contains the fix from PR #1694 (`0c8fa9f59`). DREAM.3D `v6.5.171`, `Source/Plugins/Processing/ProcessingFilters/MinNeighbors.cpp`, contains the defective path. From bff57c8f8b67fe184d5bf25222d3fa888d2bfc81 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 20:01:29 -0400 Subject: [PATCH 61/91] DOC: Rewrite the RotateSampleRefFrame V&V record to follow the report template Signed-off-by: Michael Jackson --- .../vv/RotateSampleRefFrameFilter.md | 157 ++++++++++-------- .../deviations/RotateSampleRefFrameFilter.md | 64 +++++-- .../provenance/RotateSampleRefFrameFilter.md | 83 ++++++--- 3 files changed, 201 insertions(+), 103 deletions(-) diff --git a/src/Plugins/SimplnxCore/vv/RotateSampleRefFrameFilter.md b/src/Plugins/SimplnxCore/vv/RotateSampleRefFrameFilter.md index 4cbc017cdb..a7fd16fe6f 100644 --- a/src/Plugins/SimplnxCore/vv/RotateSampleRefFrameFilter.md +++ b/src/Plugins/SimplnxCore/vv/RotateSampleRefFrameFilter.md @@ -1,105 +1,128 @@ # V&V Report: RotateSampleRefFrameFilter -| | | -|-----------|------------------| -| Plugin | SimplnxCore | +| | | +|--------|--------------| +| Plugin | SimplnxCore | | SIMPLNX UUID | `d2451dc1-a5a1-4ac2-a64d-7991669dcffc` | | SIMPLNX Human Name | Rotate Sample Reference Frame | -| DREAM3D 6.5.171 equivalent | `RotateSampleRefFrame` (SIMPL UUID `{e25d9b4c-2b37-578c-b1de-cf7032b5ef19}`) — `Source/Plugins/Sampling/SamplingFilters/RotateSampleRefFrame.{h,cpp}` | +| DREAM.3D v6.5.171 equivalent | `RotateSampleRefFrame` (Sampling plugin, SIMPL UUID `e25d9b4c-2b37-578c-b1de-cf7032b5ef19`) | | Verified commit | ** | -| Status | COMPLETE — 2026-07-16 | -| Sign-off | Michael Jackson — 2026-07-16 | +| Status | COMPLETE | +| Sign-off | Michael Jackson, 2026-07-02; second engineer: Nathan Young, 2026-07-08 | ## At a glance -| Aspect | Current state | -|------------------------|--------------------------| -| Algorithm Relationship | **Rewrite** — generalized Eigen 4×4-affine reimplementation (via shared `ImageRotationUtilities`) of the legacy hand-rolled `RotateSampleRefFrame`; different UUID (SIMPL→NX mapped), different NN rule (cell-center `computeCellIndex` vs truncation), new-geometry vs in-place, plus new `KeepInputGeometryOrigin` + Rotation-Matrix representation. | -| Oracle (confirmed) | **Class 1 (Analytical)** — exact voxel permutations: an explicit 180@Z slice reversal, **hand-derived 90-degree permutations about X/Y/Z that pin the rotation chirality** (a +90 and a -90 give different arrays, so an inverse-transform regression is caught), non-zero-origin placement, and anisotropic-spacing permutation. **Class 4 (Invariant)** — value-multiset conservation, zero background, full-circle composition = identity, and acceptance of the full octahedral group (120@(111)). Encoded in `test/RotateSampleRefFrameTest.cpp` (11 cases, all pass). | -| Code paths enumerated | 11 (filter preflight guard + geometry setup, and the NN-resample execute). 10 of 12 exercised. | -| Tests today | 12 registered cases plus one hidden HDF5 boundary test. Paired CTest selections pass 12/12. | -| Exemplar archive | **`Rotate_Sample_Ref_Frame_Test_v2/v3.tar.gz` retired** — golden-file (regression) oracle replaced by an inlined Class 1 analytical oracle. No archive is downloaded. See `vv/provenance/RotateSampleRefFrameFilter.md`. | -| Legacy comparison | **Run** (SIMPLNX vs 6.5.171) on four principal-90 fixtures (90@Z, 180@Z, 90@X, 180@Y): **bit-identical** — same dims and same voxel values. 1 deviation, on the *unsupported* arbitrary-rotation domain only. | -| Bug flags | None. The single deviation (D1) is an intentional guard, not a bug. | -| 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 | Rewrite of `RotateSampleRefFrame` in DREAM.3D v6.5.171. SIMPLNX uses shared affine transformation utilities and restricts rotations to proper signed axis permutations. | +| Oracle (confirmed) | Class 1 analytical permutations and geometry values, with Class 4 value conservation and composition checks. Nine cases in `test/RotateSampleRefFrameTest.cpp` encode these checks; all pass in CI. | +| Code paths enumerated | 15 of 29 exercised. Gaps include in-place output, defensive guards, external storage, cancellation and I/O failures. | +| Tests today | 14 test cases: 9 analytical or invariant cases, 2 rejection cases, 1 conversion case, 1 child-name regression and 1 hidden HDF5 case. | +| Exemplar archive | None. Inline analytical expectations replace the retired `Rotate_Sample_Ref_Frame_Test_v2.tar.gz` and `Rotate_Sample_Ref_Frame_Test_v3.tar.gz` archives. | +| Legacy comparison | Not run on the current algorithm. The saved DREAM.3D v6.5.171 comparison is dated 2026-07-02 and predates algorithm changes. | +| Bug flags | None. | +| V&V phase | Complete. Analytical checks and regressions for the two fixed defects are encoded. | ## Summary -`RotateSampleRefFrame` rotates the reference frame of an **Image Geometry** by nearest-neighbor resampling. V&V established that the operation is a *lossless voxel permutation* — the only correct meaning of a reference-frame rotation — **if and only if** the rotation maps the cubic voxel grid onto itself, i.e. it is a member of the octahedral rotation group (the 90/180/270-degree rotations about X/Y/Z, plus 180 about a face-diagonal and 120/240 about a body-diagonal); for any other rotation it silently produces a lossy, background-padded resample (empirically demonstrated: 45@Z turns 24 voxels into 50 with 24 introduced zeros). A preflight **guard** was added to enforce the supported domain (error `-6850`, plus `-6851` for a slice-reordering slice-by-slice combination), the golden-file exemplar was retired in favor of an inlined **Class 1 analytical-permutation** oracle (+ Class 4 invariants), and a legacy A/B on four principal-90 fixtures showed SIMPLNX is **bit-identical** to DREAM3D 6.5.171 on the standard EBSD sample transforms. +`RotateSampleRefFrameFilter` rotates Image Geometry cell data by nearest-neighbor sampling and supports whole-volume or slice-by-slice operation. Class 1 permutations and geometry values, with Class 4 conservation and composition checks, verify the supported rotations. The V&V records two fixed defects: an incorrect output origin and unwanted changes to copied child names. ## Algorithm Relationship -*Classification:* **Rewrite** (same mapped UUID — a claim of functional equivalence, defended by the deviations file). +*Classification:* Rewrite -*Evidence:* The legacy `RotateSampleRefFrame::execute()` is a hand-rolled TBB `blocked_range3d` kernel using `float rotMat[3][3]`, `MatrixMath`, in-place mutation of the DataContainer, and a truncation source-index rule (`colOld = (int64)(coord/xRes)`, old-origin assumed 0). SIMPLNX reimplements this atop the shared `ImageRotationUtilities` as a generalized Eigen `Matrix4fR` 4×4 affine transform, resamples via origin-aware `ImageGeom::computeCellIndex` on each output **cell center**, creates a **new** geometry (or a rename/delete/rename dance for in-place), parallelizes across arrays (`ParallelTaskAlgorithm`), and adds two features with no legacy equivalent: `KeepInputGeometryOrigin` (PR #1355) and an explicit **Rotation Matrix** representation. The SIMPL UUID is preserved via `SimplnxCoreLegacyUUIDMapping.hpp` + 6.4/6.5 conversion fixtures. - -*Material changes:* (1) a preflight guard restricting the accepted rotations to the well-posed lossless-grid (octahedral-group) domain (see Deviation D1); (2) a fix to the transform-derived output origin — preflight previously added the input origin to the already-absolute transformed-bounding-box min corner, double-counting it for any non-zero input origin; the persisted output origin now equals the `RotateArgs::TransformedOrigin` the resample worker samples against (no change for the common origin-(0,0,0) case). The remaining review-driven edits (dead-code removal, the `IsPrincipalAxis90Rotation`→`IsLosslessGridRotation` rename, error-message wording) are non-functional. +*Evidence:* `src/Plugins/SimplnxCore/src/SimplnxCore/SimplnxCoreLegacyUUIDMapping.hpp` maps the legacy UUID to this filter; shared Eigen affine transforms and origin-aware cell-center sampling replace the legacy matrix kernel and truncated source indices. ## Oracle -*Class:* **1 (Analytical)** primary, **4 (Invariant)** companion. Class 2/3 N/A. +*Class:* 1 (Analytical), with 4 (Invariant). -*Applied:* For a 90/180/270-degree rotation about a principal axis, nearest-neighbor resampling maps the voxel grid exactly onto itself, so the output is a closed-form permutation of the input computable by hand: a 180@Z rotation of a single Z-slice is a full reversal (`1..6 → 6..1`), and the output dimensions are a deterministic axis permutation of the input dimensions. The invariant companion asserts that a principal-90 rotation is a bijection on the voxel set (output value-multiset equals input, no `0` background introduced) and that applying the rotation until it sums to 360 degrees returns the original array and dimensions exactly. +*Applied:* Hand-derived permutations specify voxel order, dimensions, spacing and origin; value conservation and full-circle composition check the remaining positive fixtures. -*Encoded:* `test/RotateSampleRefFrameTest.cpp` — `Class 1 - 180 about Z reverses a slice` (explicit expected array, both representations); `Class 1 - exact 90-degree permutation pins chirality` (hand-derived arrays for 90@X/Y/Z, distinguishing +90 from -90); `Class 1 - non-zero input origin` (transform-derived origin = absolute transformed min corner, plus keep-origin); `Class 1 - anisotropic spacing permutes with the axes`; `Class 1/4 - principal-90 rotations are lossless permutations` (9 rotations × 2 representations: exact dims + value bijection); `Class 4 - full-circle composition is identity`; `accepts 120-degree rotation about (111)`; `KeepInputGeometryOrigin controls output origin`; `slice-by-slice 180 about Y is a lossless per-slice flip`. 11 test cases, all pass (in-core). +*Encoded:* `src/Plugins/SimplnxCore/test/RotateSampleRefFrameTest.cpp::SimplnxCore::RotateSampleRefFrame: Class 1 - 180 about Z reverses a slice` and eight companion cases include 18 principal-axis rotation variants and three chirality fixtures. 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 (technical authority). ## 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 fix for `RotateSampleRefFrameFilter-D2`. The fix for `RotateSampleRefFrameFilter-D3` will be in the DREAM3D-NX release after v7.4.2. + +| Deviation | Defect | Affected released versions | Resolution | +|-----------|--------|----------------------------|------------| +| `RotateSampleRefFrameFilter-D2` | The transform-derived output origin counted the nonzero input origin twice. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | The output origin equals the absolute minimum corner of the transformed bounding box. | +| `RotateSampleRefFrameFilter-D3` | Copying geometry children replaced the geometry name inside child names and parent paths. | DREAM3D-NX v7.0.0 through v7.4.2. DREAM.3D v6.5.171 was not affected. | Copy actions replace only the source geometry path prefix and preserve child names. | ## Code path coverage -*10 of 12 paths exercised. The 2 gaps are a defensive out-of-bounds branch (unreachable on the guarded lossless-rotation domain, where every output cell maps in-bounds by construction) and the cancel-signal branches (require mid-execution cancel injection). Neither is algorithmic logic.* - -Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/RotateSampleRefFrameFilter.cpp` (preflight guard + geometry setup) and `src/SimplnxCore/Filters/Algorithms/RotateSampleRefFrame.cpp` (112 lines) delegating to `src/simplnx/Utilities/ImageRotationUtilities.{hpp,cpp}` (nearest-neighbor path). Logical phases: (a) preflight — validate rotation + build output geometry/actions; (b) execute — per-array nearest-neighbor resample. - -| # | Phase | Path | Test case | -|----|--------------|-------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------| -| 1 | (a) Preflight| Rotation is a proper signed-permutation (octahedral group) → proceed | all valid tests (`Class 1`, `Class 1/4`, composition, origin, spacing, 120@(111)) | -| 2 | (a) Preflight| Rotation is not a lossless grid rotation → error `-6850` | `rejects non-principal-90 rotations` (45@Z, 90@(1,1,1), arbitrary matrix) | -| 3 | (a) Preflight| slice-by-slice + rotation does not preserve Z (\|R(2,2)\|≠1) → error `-6851` | `rejects slice-by-slice with a slice-reordering rotation` (90@X + slice-by-slice) | -| 4 | (a) Preflight| Representation = Axis-Angle → `GenerateRotationTransformationMatrix` | all Axis-Angle sections | -| 5 | (a) Preflight| Representation = Rotation Matrix → `GenerateManualTransformationMatrix` | `Class 1` + `Class 1/4` Rotation-Matrix sections | -| 6 | (b) Execute | Nearest-neighbor resample, slice-by-slice = false (3D rotation) | `Class 1`, `Class 1/4`, `full-circle composition` | -| 7 | (b) Execute | Nearest-neighbor resample, slice-by-slice = true, Z-preserving → per-slice flip | `slice-by-slice 180 about Y is a lossless per-slice flip` | -| 8 | (b) Execute | Inverse-mapped coordinate out of bounds → `fillTuple(destIndex, 0)` | *Not directly tested. Unreachable on the guarded lossless-rotation domain (exact permutation maps every cell in-bounds); defensive branch.* | -| 9 | (a) Preflight| `RemoveOriginalGeometry = true` → rename/delete/rename in-place | legacy A/B pipelines (nxrunner, `remove_original_geometry=true`) + shipping pipelines | -| 10 | (b) Execute | `KeepInputGeometryOrigin` true (keep src origin) vs false (transform-derived origin) | `KeepInputGeometryOrigin controls output origin` (both branches, exact values) | -| 11 | (b) Execute | Cancel checks (`m_ShouldCancel` in the array loop; `getCancel()` per output slice) | *Not tested. Requires cancel-signal injection; low-value.* | -| 12 | OOC boundary | 1MiB-page permutation oracle | `real HDF5 1MiB-page permutation oracle` — independent expected outputs on actual HDF5 stores | +15 of 29 paths exercised. Each uncovered row states the gap. + +Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/RotateSampleRefFrameFilter.cpp` (406 lines); `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/RotateSampleRefFrame.cpp` (108 lines); `src/simplnx/Utilities/ImageRotationUtilities.hpp` (1345 lines); `src/simplnx/Utilities/ImageRotationUtilities.cpp` (288 lines). + +Test names below omit the `SimplnxCore::RotateSampleRefFrame: ` prefix. Coverage refers to the mapped in-core tests and the nearest-neighbor code used by this filter. + +| # | Phase | Path | Test case | +|---|-------|------|-----------| +| 1 | Preflight | Accept a proper signed axis permutation | `accepts 120-degree rotation about (111)`; `Class 1/4 - principal-90 rotations are lossless permutations` | +| 2 | Preflight | Reject a matrix entry outside the allowed tolerance with `-6850` | `rejects non-principal-90 rotations` — two axis-angle cases and one matrix case | +| 3 | Preflight | Reject a matrix without exactly one signed unit entry in each row and column | *Not directly tested. No fixture supplies a degenerate matrix with only zero and signed unit entries.* | +| 4 | Preflight | Reject a reflection with determinant -1 | *Not directly tested. No fixture supplies a reflection matrix.* | +| 5 | Preflight | Reject slice-by-slice rotation that exchanges Z with another axis with `-6851` | `rejects slice-by-slice with a slice-reordering rotation` | +| 6 | Transform | Construct the matrix from axis-angle parameters | `Class 1 - exact 90-degree permutation pins chirality` — X, Y and Z | +| 7 | Transform | Read the supplied rotation matrix | `Class 1 - 180 about Z reverses a slice`; `Class 1/4 - principal-90 rotations are lossless permutations` — matrix variants | +| 8 | Sampling | Inverse-map cell centers for a whole-volume rotation | `Class 1 - exact 90-degree permutation pins chirality`; `Class 4 - full-circle composition is identity` | +| 9 | Sampling | Keep source Z equal to output Z in slice-by-slice mode | `slice-by-slice 180 about Y is a lossless per-slice flip` — value conservation and different results from whole-volume mode | +| 10 | Geometry | Keep the input origin | `Class 1 - non-zero input origin` — keep-origin section | +| 11 | Geometry | Use the transformed bounding-box origin | `Class 1 - non-zero input origin` — expects (-22,10,0) and exact voxel values | +| 12 | Geometry | Create output at a separate geometry path | `Class 1 - 180 about Z reverses a slice` | +| 13 | Geometry | Replace the input geometry through deferred rename and delete actions | *Not directly tested. Current test executions create separate output geometries.* | +| 14 | Copy | Copy an additional group and declare its descendant paths | `Child names containing the geometry name are kept` — declared paths and copied objects | +| 15 | Copy | Copy an additional non-group child | *Not directly tested. The child-name fixture adds an AttributeMatrix, not a direct array child.* | +| 16 | Copy | No extra geometry children require copying | `Class 1 - 180 about Z reverses a slice` | +| 17 | Preflight | Warn that copied feature AttributeMatrices can contain stale data | *Not directly tested. The child-name regression does not assert the warning.* | +| 18 | Storage | Read source slabs, reuse overlapping slices and write output slices | `Class 1/4 - principal-90 rotations are lossless permutations`; `Class 4 - full-circle composition is identity` | +| 19 | Storage | Use bounded source pages and serialize array tasks | *Not directly tested. The hidden `real HDF5 1MiB-page permutation oracle` requires external stores.* | +| 20 | Sampling | A mapped coordinate is outside the source geometry; retain zero fill | *Not directly tested. Current fixtures map all output cell centers inside the source.* | +| 21 | Sampling | A whole output slice is outside the source; write zeros | *Not directly tested. No fixture produces an exterior output slice.* | +| 22 | Cancellation | Stop before an array task or output slice | *Not directly tested. No fixture injects cancellation.* | +| 23 | Storage | Return a source read, page range or page allocation error | *Not directly tested. No fixture injects these failures.* | +| 24 | Storage | Return a destination slice write error | *Not directly tested. No fixture injects a write failure.* | +| 25 | Dispatch | Rotate a scalar Int32 cell array | `Class 1 - 180 about Z reverses a slice` | +| 26 | Dispatch | Rotate another scalar type or a multi-component array | *Not directly tested. All rotation fixtures use scalar Int32 cell data.* | +| 27 | Geometry | Permute unequal X and Y spacing for 90 degrees about Z | `Class 1 - anisotropic spacing permutes with the axes` — checks (5,2,1) | +| 28 | Geometry | Clamp a computed zero output dimension to one | *Not directly tested. Fixtures have nonzero dimensions and positive spacing.* | +| 29 | Sampling | A valid source Z is outside the cached slab; retain zero fill | *Not directly tested. The calculated slab contains the source cells used by the fixtures.* | ## Test inventory +The 13 ordinary cases run in in-core CI. All pass in CI. The hidden case is listed separately in the inventory. + | Test case | Status | Notes | |-----------|--------|-------| -| `SimplnxCore::RotateSampleRefFrame: Class 1 - 180 about Z reverses a slice` | new-for-V&V | Explicit hand-derived expected array `[6,5,4,3,2,1]` on a 3×2×1 volume; Axis-Angle + Rotation-Matrix sections (the matrix section incidentally cross-checks `GenerateRotationTransformationMatrix` against an Eigen-built matrix). | -| `SimplnxCore::RotateSampleRefFrame: Class 1 - exact 90-degree permutation pins chirality` | new-for-V&V | Hand-derived exact output arrays for 90@Z (`[4,1,5,2,6,3]`), 90@X, 90@Y. Distinguishes +90 from -90, so a regression that dropped the inverse-transform (forward matrix instead of its inverse) is caught. Derivation method validated by reproducing the 180@Z result. | -| `SimplnxCore::RotateSampleRefFrame: Class 1 - non-zero input origin` | new-for-V&V | Input origin (10,20,0), 90@Z → transform-derived origin (-22,10,0) (absolute transformed min corner), not the double-counted (-12,30,0); keep-origin branch preserves the input origin. Both branches also assert the value permutation is origin-invariant. | -| `SimplnxCore::RotateSampleRefFrame: Class 1 - anisotropic spacing permutes with the axes` | new-for-V&V | Input spacing (2,5,1), 90@Z → output spacing (5,2,1); pins the spacing permutation that isotropic fixtures cannot detect. | -| `SimplnxCore::RotateSampleRefFrame: accepts 120-degree rotation about (111)` | new-for-V&V | Confirms the guard accepts the full octahedral group (not just principal-90): 120@(111) is a lossless value bijection. Documents that the enforced domain is broader than "principal-axis 90s". | -| `SimplnxCore::RotateSampleRefFrame: Class 1/4 - principal-90 rotations are lossless permutations` | new-for-V&V | 9 rotations (90/180/270 × X/Y/Z) × 2 representations = 18 DYNAMIC_SECTIONs; asserts exact permuted output dims + value-multiset bijection + zero background. | -| `SimplnxCore::RotateSampleRefFrame: Class 4 - full-circle composition is identity` | new-for-V&V | 4×90@Z, 2×180@Z, 4×90@X, 4×90@Y each return the original array + dims exactly. | -| `SimplnxCore::RotateSampleRefFrame: KeepInputGeometryOrigin controls output origin` | new-for-V&V | 90@Z on a 4×3×2 at origin (0,0,0): keep → (0,0,0); transform-derived → (-3,0,0). | -| `SimplnxCore::RotateSampleRefFrame: slice-by-slice 180 about Y is a lossless per-slice flip` | new-for-V&V | Value bijection for the slice-by-slice flip, and asserts it differs from the true 3D rotation (slice order preserved vs reversed). | -| `SimplnxCore::RotateSampleRefFrame: rejects non-principal-90 rotations` | new-for-V&V | 45@Z, 90@(1,1,1), and an arbitrary 45° Rotation Matrix each return preflight error `-6850`. | -| `SimplnxCore::RotateSampleRefFrame: rejects slice-by-slice with a slice-reordering rotation` | new-for-V&V | 90@X + slice-by-slice returns preflight error `-6851`. | -| `SimplnxCore::RotateSampleRefFrameFilter: SIMPL Backwards Compatibility` | kept | Unchanged. DYNAMIC_SECTION over SIMPL 6.4 + 6.5 conversion fixtures; validates UUID + argument keys. | -| `real HDF5 1MiB-page permutation oracle` | new-for-V&V | 528,425 assertions; The existing sequential-image builder creates 257×257×4 values. A half turn about Z must reverse each XY plane without changing Z order; every output value is checked. The 264,196 int32 values cross a full 262,144-element source-cache page and a partial second page. Both whole-volume and slice modes use HDF5 input/output and target-only OOC dispatch witnesses. | -| `slice-by-slice 180 about Y is a lossless per-slice flip` | kept | Existing per-slice versus whole-volume permutation and bijection regression. | - -All non-retired tests pass in-core. Full `NX-Com-Qt69-Vtk96-Rel` build: affected tests pass, including `ReadH5Ebsd`, all EBSD readers, `ITKImportImageStack`, and this filter's SIMPL compat test. OOC dual-build run outstanding (guard is preflight-only; negligible OOC risk). - -OOC recertification (2026-09-21): serial CTest passed 12/12 target entries in each DREAM3DNX build. The hidden `real HDF5 1MiB-page permutation oracle` passed 528,425 assertions in the OOC binary. The existing sequential-image builder creates 257×257×4 values. A half turn about Z must reverse each XY plane without changing Z order; every output value is checked. The 264,196 int32 values cross a full 262,144-element source-cache page and a partial second page. Both whole-volume and slice modes use HDF5 input/output and target-only OOC dispatch witnesses. 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. +| `SimplnxCore::RotateSampleRefFrame: Class 1 - 180 about Z reverses a slice` | new-for-V&V | Checks [6,5,4,3,2,1] on a 3×2×1 input in Axis-Angle and Rotation-Matrix sections. | +| `SimplnxCore::RotateSampleRefFrame: Class 1 - exact 90-degree permutation pins chirality` | new-for-V&V | Three variants check exact dimensions and arrays for 90 degrees about Z, X and Y. The expected arrays distinguish positive from negative rotations. | +| `SimplnxCore::RotateSampleRefFrame: Class 1 - anisotropic spacing permutes with the axes` | new-for-V&V | Checks that 90 degrees about Z changes spacing (2,5,1) to (5,2,1). | +| `SimplnxCore::RotateSampleRefFrame: accepts 120-degree rotation about (111)` | new-for-V&V | Checks all 24 values form a bijection on a 2×3×4 input with unit spacing. | +| `SimplnxCore::RotateSampleRefFrame: Class 1 - non-zero input origin` | new-for-V&V | Checks exact voxel values and origin (-22,10,0), or the preserved input origin (10,20,0). Covers D2. | +| `SimplnxCore::RotateSampleRefFrame: Class 1/4 - principal-90 rotations are lossless permutations` | new-for-V&V | Nine rotations (90, 180 and 270 degrees about X, Y and Z), each in both representations. Checks dimensions, all input values exactly once, and no zero fill. | +| `SimplnxCore::RotateSampleRefFrame: Class 4 - full-circle composition is identity` | new-for-V&V | Four variants: four 90-degree rotations about Z, X or Y, and two 180-degree rotations about Z. Checks the original dimensions and every original value. | +| `SimplnxCore::RotateSampleRefFrame: KeepInputGeometryOrigin controls output origin` | new-for-V&V | Checks (0,0,0) with keep-origin enabled and (-3,0,0) with it disabled. | +| `SimplnxCore::RotateSampleRefFrame: slice-by-slice 180 about Y is a lossless per-slice flip` | new-for-V&V | Checks value conservation in both slice and whole-volume modes and requires their output arrays to differ. | +| `SimplnxCore::RotateSampleRefFrame: rejects non-principal-90 rotations` | new-for-V&V | Checks `-6850` for 45 degrees about Z, 90 degrees about (111), and a 45-degree rotation matrix. | +| `SimplnxCore::RotateSampleRefFrame: rejects slice-by-slice with a slice-reordering rotation` | new-for-V&V | Checks `-6851` for 90 degrees about X with slice-by-slice enabled. | +| `SimplnxCore::RotateSampleRefFrame: real HDF5 1MiB-page permutation oracle` | new-for-V&V | Hidden. Checks HDF5 input and output stores, dispatch counters, and all 264,196 values against a per-plane reversal formula in both slice modes. | +| `SimplnxCore::RotateSampleRefFrameFilter: SIMPL Backwards Compatibility` | kept | Two dynamic variants convert SIMPL 6.4 and 6.5 fixtures and check filter identity, comments and selected arguments. | +| `SimplnxCore::RotateSampleRefFrame: Child names containing the geometry name are kept` | kept | Checks declared descendant paths and copied object names when `Geom` contains `Geom Feature Data/Geom Values`. Covers D3. | ## Exemplar archive -- **Archive:** None. The prior golden-file exemplars `Rotate_Sample_Ref_Frame_Test_v2.tar.gz` / `_v3.tar.gz` were **retired** this V&V cycle (a regression-on-prior-output oracle) and replaced by an inlined Class 1 analytical oracle. The `download_test_data()` entries were removed from `test/CMakeLists.txt`. -- **Provenance:** `src/Plugins/SimplnxCore/vv/provenance/RotateSampleRefFrameFilter.md` (documents the retirement, the inline oracle, and the legacy A/B inputs). +- **Archive:** None. The v2 and v3 rotation archives are retired; the oracle is inline in the test source. +- **SHA512:** Not applicable. `src/Plugins/SimplnxCore/test/CMakeLists.txt` has no active archive registration for this filter. +- **Provenance:** `src/Plugins/SimplnxCore/vv/provenance/RotateSampleRefFrameFilter.md` -## Deviations from DREAM3D 6.5.171 +## Deviations from DREAM.3D v6.5.171 -Comparison run on four principal-90 fixtures (90@Z, 180@Z, 90@X, 180@Y) — see `vv/comparisons/RotateSampleRefFrameFilter/results.md`. On the supported domain, **no deviations** (bit-identical dims and voxel values). One deviation on the unsupported domain: +See `vv/deviations/RotateSampleRefFrameFilter.md` for the root cause, affected users and recommendation. This table lists only Part 1. -- `RotateSampleRefFrameFilter-D1` — legacy silently runs arbitrary (non-principal-90) rotations as a lossy resample; SIMPLNX rejects them in preflight (`-6850`). Root cause: algorithmic choice (intentional; not a bug). See `vv/deviations/RotateSampleRefFrameFilter.md`. +| Deviation | Observed difference | +|-----------|---------------------| +| `RotateSampleRefFrameFilter-D1` | SIMPLNX rejects rotations outside the proper signed axis permutations (`-6850`) and rejects slice-by-slice rotations that exchange Z with another axis (`-6851`). DREAM.3D v6.5.171 has neither guard. | diff --git a/src/Plugins/SimplnxCore/vv/deviations/RotateSampleRefFrameFilter.md b/src/Plugins/SimplnxCore/vv/deviations/RotateSampleRefFrameFilter.md index 2e79c0da0f..35461bb638 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/RotateSampleRefFrameFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/RotateSampleRefFrameFilter.md @@ -1,27 +1,69 @@ -# Deviations from DREAM3D 6.5.171: RotateSampleRefFrameFilter +# Deviations from DREAM.3D v6.5.171: RotateSampleRefFrameFilter -This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent. +Part 1 describes the supported-rotation difference from DREAM.3D v6.5.171. Part 2 records two fixed SIMPLNX defects. All entries use one stable ID sequence and the filter UUID as a permanent reference. -Entries are referenced by stable ID (`RotateSampleRefFrameFilter-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 -Legacy equivalent: `RotateSampleRefFrame` (SIMPL UUID `{e25d9b4c-2b37-578c-b1de-cf7032b5ef19}`) in the Sampling plugin. On the **supported domain** (a 90/180/270-degree rotation about the X, Y, or Z axis) SIMPLNX and 6.5.171 are **bit-identical** — same output dimensions and same voxel values on all four A/B fixtures (see `../comparisons/RotateSampleRefFrameFilter/results.md`). The single deviation below is about rotations *outside* that domain. +### RotateSampleRefFrameFilter-D1 + +| Field | Value | +|---|---| +| **Deviation ID** | `RotateSampleRefFrameFilter-D1` | +| **Filter UUID** | `d2451dc1-a5a1-4ac2-a64d-7991669dcffc` | +| **Status** | active | + +**Symptom:** DREAM.3D v6.5.171 accepts arbitrary rotation angles. SIMPLNX returns `-6850` for rotations outside the proper signed axis permutations and `-6851` when slice-by-slice operation exchanges Z with another axis. + +**Root cause:** Algorithmic choice. SIMPLNX checks the rotation block for one signed unit entry per row and column and a determinant of +1, within tolerance `1e-4`. This includes 120-degree rotations about (111), as well as principal-axis quarter turns. The slice guard also requires the Z axis to remain on Z. Arbitrary nearest-neighbor rotations can duplicate or omit source cells and introduce zero background. DREAM.3D v6.5.171 has neither guard. DREAM3D-NX v7.4.2 contains both guards. + +**Affected users:** Users who request arbitrary rotations or combine a slice-axis exchange with slice-by-slice operation. `ReadH5Ebsd` calls this filter in slice-by-slice mode for recommended sample transformations and propagates its errors during import. + +**Recommendation:** Trust SIMPLNX for the supported rotations. Use `Apply Transformation To Geometry` for arbitrary resampling. Disable `Use Recommended Transformations` in `ReadH5Ebsd` when the stored transform cannot use slice-by-slice operation, and apply the required transformation separately. + +**Evidence:** `RotateSampleRefFrameFilter.cpp::IsLosslessGridRotation` and `preflightImpl()` implement the guards. The two rejection tests check `-6850` and `-6851`; the 120-degree fixture checks acceptance and value conservation. Tagged DREAM.3D v6.5.171 source has no equivalent validation. `vv_work:RotateSampleRefFrameFilter/results.md` records four matching principal-axis cases on 2026-07-02; subsequent algorithm changes are listed in the provenance sidecar. --- -## RotateSampleRefFrameFilter-D1 +## Part 2 — SIMPLNX defects and behavior changes + +### RotateSampleRefFrameFilter-D2 | Field | Value | |---|---| -| **Deviation ID** | `RotateSampleRefFrameFilter-D1` | +| **Deviation ID** | `RotateSampleRefFrameFilter-D2` | +| **Filter UUID** | `d2451dc1-a5a1-4ac2-a64d-7991669dcffc` | +| **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 rotation with a nonzero input origin placed the output geometry at the wrong origin when the filter used the transform-derived origin. + +**Root cause:** Bug. Preflight added the input origin to the minimum corner of the transformed bounding box. That corner already included the input origin. Preflight now assigns the corner directly, so the stored origin agrees with the geometry used for sampling. DREAM.3D v6.5.171 does not add an input origin to an absolute transformed corner and was not affected by this defect. + +**Affected users:** Users with a nonzero input origin who use the transform-derived output origin. For a 3×2×1 input at (10,20,0), a 90-degree Z rotation previously stored (-12,30,0) instead of (-22,10,0). + +**Recommendation:** Trust SIMPLNX in DREAM3D-NX v7.4.2 or later. The output origin now uses the transformed bounding box once. + +**Evidence:** Direct inspection of every release source from `upstream/release/v7.0.0` through `upstream/release/v7.4.1` shows `origin +=` with origin-aware bounds; v7.4.2 assigns the bounds. PR #1651 (`790d9ca18`) contains the fix. `Class 1 - non-zero input origin` checks both origin modes and exact voxel values. Tagged legacy `Source/Plugins/Sampling/SamplingFilters/RotateSampleRefFrame.cpp` constructs coordinates without the input origin and calls `setOrigin(xMin, yMin, zMin)`. + +--- + +### RotateSampleRefFrameFilter-D3 + +| Field | Value | +|---|---| +| **Deviation ID** | `RotateSampleRefFrameFilter-D3` | | **Filter UUID** | `d2451dc1-a5a1-4ac2-a64d-7991669dcffc` | | **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:** For a rotation that does *not* map the voxel grid onto itself — e.g. 45 degrees about Z, or 90 degrees about (111) — DREAM3D 6.5.171 runs and produces an output, whereas SIMPLNX rejects the rotation in preflight with error `-6850` (and rejects slice-by-slice on a slice-reordering rotation with `-6851`). Note the enforced domain is the full octahedral rotation group, so lossless off-principal rotations (180° about (110), 120°/240° about (111)) are *accepted*, not rejected. +**Symptom:** Copying extra geometry children could change their names or produce an invalid destination path when a child or parent name contained the source geometry name. -**Root cause:** Algorithmic choice. A reference-frame rotation is only a lossless re-labeling of the voxels when the rotation maps the grid onto itself (a signed axis-permutation / octahedral-group rotation). For any other rotation the nearest-neighbor resample is lossy: it enlarges the bounding box, drops and duplicates voxels, and pads the result with background (0) values — 45° about Z of a 4×3×2 test volume, for example, turns 24 voxels into 50 with 24 introduced zeros. Legacy 6.5.171 performed this lossy resample silently (its documentation only *hedged* that the filter was "verified" for 90/180 about a principal axis). SIMPLNX makes the supported domain an enforced contract via a preflight guard (`RotateSampleRefFrameFilter.cpp`, `IsLosslessGridRotation`), so the arbitrary-rotation path can no longer be reached — arguably a latent-bug fix for a filter whose purpose is a reference-frame rotation. +**Root cause:** Bug. Preflight replaced every occurrence of the geometry name in each complete path string. For example, copying `Geom/Geom Feature Data/Geom Values` to `Geom Out` also changed both child names. `DataPath::rebase()` now changes only the source geometry path prefix, including the declared recursive paths. DREAM.3D v6.5.171 rotates its selected container in place and has no corresponding child-copy path. -**Affected users:** Anyone who previously fed a lossy rotation to the legacy filter (an undocumented/unsupported use). Pipelines that used the standard EBSD sample transforms (n×90 about a principal axis, e.g. 180° about Y) are unaffected — those remain bit-identical. +**Affected users:** Users whose geometry has additional children, or parent groups, with names that contain the geometry name. The copied object names or their destination paths could change unexpectedly. -**Blast radius — internal callers.** `ReadH5Ebsd` (`ReadH5Ebsd.cpp`) forwards the sample-reference-frame transform stored in the `.h5ebsd` file directly into this filter with slice-by-slice enabled, and propagates preflight errors. If a file's *recommended* sample transform is not a lossless, Z-preserving rotation (for example a stored 90° about X, which trips `-6851`, or any non-group angle, which trips `-6850`), the entire H5EBSD import now fails preflight with an error naming a filter the user never placed. The prior behavior was a silently corrupted (slice-pinned) import, so failing loudly is arguably correct, but the message can be confusing in that context. The practical workaround is to uncheck *Use Recommended Transformations* in `ReadH5Ebsd` (or apply the transform separately). This is called out here so the failure mode is documented rather than surprising. +**Recommendation:** Trust SIMPLNX with the fix in the DREAM3D-NX release after v7.4.2. On affected releases, use names that do not repeat the geometry name in parent or child components. -**Recommendation:** Trust SIMPLNX. For an arbitrary rotation with interpolation, use the `Apply Transformation To Geometry` filter, which is designed for lossy resampling rotations (nearest-neighbor or trilinear). The legacy result for a lossy rotation was a silent lossy resample, not a faithful reference-frame rotation. For H5EBSD imports whose recommended transform is lossy, uncheck *Use Recommended Transformations* in `ReadH5Ebsd`. +**Evidence:** Each release source from `upstream/release/v7.0.0` through `upstream/release/v7.4.2` uses `StringUtilities::replace()` for copied paths. PR #1765 (`1986127eb`) replaces both sites with `DataPath::rebase()`. `Child names containing the geometry name are kept` checks declared paths and copied objects. Tagged DREAM.3D v6.5.171 source has no corresponding copy operation. 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) | From 53effeae039c33534baf5d4d553bb54ad61adb46 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Oct 2026 20:01:29 -0400 Subject: [PATCH 62/91] DOC: Rewrite the WriteDREAM3D V&V record to follow the report template Signed-off-by: Michael Jackson --- .../SimplnxCore/vv/WriteDREAM3DFilter.md | 215 ++++++++---------- .../vv/deviations/WriteDREAM3DFilter.md | 78 +++++-- 2 files changed, 157 insertions(+), 136 deletions(-) diff --git a/src/Plugins/SimplnxCore/vv/WriteDREAM3DFilter.md b/src/Plugins/SimplnxCore/vv/WriteDREAM3DFilter.md index 227ce94781..3429f9f3ea 100644 --- a/src/Plugins/SimplnxCore/vv/WriteDREAM3DFilter.md +++ b/src/Plugins/SimplnxCore/vv/WriteDREAM3DFilter.md @@ -1,156 +1,129 @@ # V&V Report: WriteDREAM3DFilter -| | | -|-----------|------------------| -| Plugin | SimplnxCore | -| SIMPLNX UUID | `b3a95784-2ced-41ec-8d3d-0242ac130003` | -| SIMPLNX Human Name | Write DREAM3D-NX File | -| DREAM3D 6.5.171 equivalent | `DataContainerWriter` — SIMPL UUID `3fcd4c43-9d75-5b86-aad4-4441bc914f37` | -| Verified commit | ** | -| Status | COMPLETE — 2026-08-20 | -| Sign-off | Matthew Marine (V&V author, PR #1683). Second engineer: Michael A. Jackson , 2026-08-20 (PR #1683 review). | +| | | +|--------|--------------| +| Plugin | SimplnxCore | +| SIMPLNX UUID | `b3a95784-2ced-41ec-8d3d-0242ac130003` | +| SIMPLNX Human Name | Write DREAM3D-NX File | +| DREAM.3D v6.5.171 equivalent | `DataContainerWriter` (SIMPL UUID `3fcd4c43-9d75-5b86-aad4-4441bc914f37`) | +| Verified commit | ** | +| Status | COMPLETE | +| Sign-off | Matthew Marine, 2026-07-23; second engineer: Michael Jackson, 2026-08-20 | ## At a glance -| Aspect | Current state | -|------------------------|--------------------------| -| Algorithm Relationship | **Rewrite** — same UUID/role as legacy `DataContainerWriter`, but the on-disk format is entirely new (v8 `DataStructure` HDF5 layout + `AtomicFile` atomic-write + optional gzip compression), not a translation of the legacy writer's code. | -| Oracle (confirmed) | **Class 1 (Analytical)** — expected content is the hand-built in-memory `DataStructure`/`Pipeline` the test itself constructed; expected HDF5 physical layout (contiguous vs. chunked+deflate) is a closed-form function of array byte-size and the two compression parameters. 20 Write-related fixtures across `DREAM3DFileTest.cpp`, all pass. | -| Code paths enumerated | **16 of 20** exercised; the 4 remaining gaps are defensive/unreachable-via-public-API guards (see table). | -| Tests today | 22 registered entries in the focused selection, plus one hidden HDF5 boundary case. Paired selections pass 22/22. | -| Exemplar archive | **None.** Every test builds its `DataStructure` inline in C++ and round-trips it through `WriteFile`/`ReadFile` in the same run — no cached `.tar.gz` golden file is used or needed for a Class 1 oracle. | -| Legacy comparison | **Not run — and not applicable.** The two writers target deliberately different on-disk contracts, so a byte/dataset-level A/B against 6.5.171 `DataContainerWriter` output would be 100% noise by design, not signal. `ReadDREAM3DFilter` is the only tool in either codebase that understands both formats; fidelity is instead verified independently via round-trip Class 1 tests. | -| Bug flags | One, since resolved: `WriteXdmfNodeGeometry1D/2D/3D` (`Dream3dIO.cpp`) forwarded to the next-lower writer with `geomName` and `hdf5FilePath` transposed (both `std::string_view`, so it compiled silently), producing `.xdmf` node-attribute references that ParaView/VisIt could not resolve. Fixed alongside a content-level `.xdmf` oracle (`CheckXdmfFile`) that would have caught it. | -| 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 DREAM.3D v6.5.171 `DataContainerWriter`. SIMPLNX writes the version 8 HDF5 layout, with atomic file replacement and optional compression. | +| Oracle (confirmed) | Class 1 constructed values, HDF5 layout checks and XDMF filename checks, with a Class 4 compression-size check. The ordinary tests in `test/DREAM3DFileTest.cpp` encode these checks. All pass in CI. | +| Code paths enumerated | 15 of 21 exercised. Failure injection, detached-pipeline, cancellation and hidden storage-boundary paths have no ordinary-test witness. | +| Tests today | 24 test cases in the shared file: 23 ordinary cases and 1 hidden storage-boundary case. They cover writing, reading, compression, geometry ownership and SIMPL conversion. | +| Exemplar archive | None for writer verification. The writer fixtures construct their input and expected values in C++. | +| Legacy comparison | Not run — comparison artifacts are not available. | +| Bug flags | None | +| V&V phase | COMPLETE. | ## Summary -`WriteDREAM3DFilter` serializes the current `DataStructure` (and, when run inside a pipeline, the preceding `Pipeline`) to an HDF5 `.dream3d` file, with an optional companion `.xdmf` sidecar and optional gzip compression of array datasets. It replaces legacy SIMPL's `DataContainerWriter` under the same conceptual role but with an intentionally new v8 file format, so verification is independent of 6.5.171: correctness is established by writing hand-built `DataStructure`s covering every geometry and `DataObject` type, then reading them back and asserting exact structural/content equality (Class 1 Analytical), plus closed-form assertions on the resulting HDF5 physical layout under each compression setting. All 20 test cases pass. One defect is resolved: the `.xdmf` node-geometry writers in the shared `Dream3dIO` utility transposed the geometry name and HDF5 file path when forwarding between levels, producing sidecar attribute references ParaView could not resolve (see Bug flags above). `StatsDataArray`/`StructArray` (SIMPL's per-ensemble statistics types) are out of scope for this cycle — those `DataObject` types do not yet exist in this branch of simplnx (see deviation D2). Separately, two `DataObject` types have no registered HDF5 IO factory and therefore cannot be written by this or any filter: a bare `DynamicListArray` (as opposed to its `NeighborList` specialization), and `GridMontage` — whose IO class exists but is never registered even though `CreateGridMontageAction` is public core API. Both are recorded under Code path coverage as observations about current behavior; neither is presented as a defect with a known fix, and montage support in particular is an unsettled design question this report takes no position on. The resulting write-failure contract is pinned by a dedicated test. +`WriteDREAM3DFilter` saves a DataStructure and its preceding pipeline in a `.dream3d` file, with optional compression and an XDMF sidecar. Constructed values and direct HDF5 layout checks verify the writer; the ordinary tests all pass in CI. DREAM3D-NX v7.4.2 contains the two shared-I/O defect fixes found during V&V. ## Algorithm Relationship -*Classification:* **Rewrite** ~~| Port | Minor changes | New filter~~ +*Classification:* Rewrite -`WriteDREAM3DFilter` keeps the SIMPL UUID mapping (`3fcd4c43-9d75-5b86-aad4-4441bc914f37` → `WriteDREAM3DFilter`, `SimplnxCoreLegacyUUIDMapping.hpp:170`) and the legacy `DataContainerWriter` role, but the algorithm (`Algorithms/WriteDREAM3D.cpp`, 82 lines) was designed from the start for the current v8 `DataStructure` HDF5 layout, `AtomicFile`-based atomic writes, and (as of PR #1606) optional gzip compression — none of which exist in the legacy 6.5.171 writer. This has never been a line-by-line port of the legacy C++; the file format itself is a clean-sheet design (`k_CurrentFileVersion = "8.0"` vs. legacy's `"7.0"`/`DataContainers` group tag, see `Dream3dIO.hpp:31`). - -*Evidence:* `parametersVersion()` is at 2 (compression parameters added after the filter's initial release); `git log --follow` on the algorithm/filter files shows the write path has been restructured multiple times since inception (out-of-core support #1253, atomic-file rework #900, algorithm-class extraction #1544) without ever tracking legacy DataContainerWriter's implementation. - -*Port-time deltas (SIMPL → SIMPLNX argument mapping, `WriteDREAM3DFilter::FromSIMPLJson`):* - -1. `OutputFile` → `export_file_path`, `WriteXdmfFile` → `write_xdmf_file`: direct 1:1 mapping, no behavior change. -2. SIMPL's "Write Time Series" parameter has no SIMPLNX equivalent (dropped) — legacy time-series writing is not part of this filter's scope in NX; not a regression since no NX pipeline concept maps to it. -3. `use_compression` is force-overridden to `false` for any pipeline converted from SIMPL JSON (`WriteDREAM3DFilter.cpp:130`), even though SIMPLNX's own default is `true`. This is deliberate: SIMPL v6 pipelines never wrote compressed files, so a converted pipeline preserves the exact on-disk encoding it shipped with rather than silently changing file size/behavior on re-run. - -*Material PRs since baseline:* #1606 (added HDF5 compression parameters/behavior), #1544 (moved `executeImpl` logic into the `WriteDREAM3D` algorithm class, no behavior change), #1253 (out-of-core support). +*Evidence:* `SimplnxCoreLegacyUUIDMapping.hpp` maps `DataContainerWriter` to this filter; `Dream3dIO.hpp` defines version 8.0 with a `DataStructure` layout instead of the legacy version 7.0 `DataContainers` layout. ## Oracle -*Class:* **1 (Analytical)**, primary. `Compression_LevelsRoundTrip` also carries a **Class 4 (Invariant)** companion check (file size must be non-increasing as gzip level rises) alongside its per-level content round-trip. - -*Applied:* Every test constructs its expected answer directly, without ever running the filter to "produce" the expected value: +*Class:* 1 (Analytical), with 4 (Invariant) companions. -- **Content fidelity:** each test builds a `DataStructure` in C++ (`CreateTestDataStructure()`, or an inline array/geometry), writes it, reads it back, and asserts the read-back content equals what was built — by construction, not by comparison to a previously-captured file. `CheckTestDataStructure()` walks every `DataObject` kind the filter must support (nested `DataGroup`s, `AttributeMatrix`, `NeighborList`, `StringArray`, and all eight geometry types: Vertex/Edge/Triangle/Quad/Tetrahedral/Hexahedral/Image/RectGrid) and asserts exact values against the hand-known fill pattern from `FillDataStore()`. -- **Xdmf sidecar:** `CheckXdmfFile()` asserts the sidecar exists and that every heavy-data reference in it (`:/DataStructure/` DataItems) resolves to the `.dream3d` file that was written — a content-level check, not an existence-only check. The test fixture includes a vertex `AttributeMatrix` on a node geometry so node-centered attribute references are actually emitted and validated. -- **HDF5 physical layout:** the filter's documented compression policy (`docs/WriteDREAM3DFilter.md`) states arrays under 16 KiB always stay contiguous/uncompressed regardless of settings, and any larger array is chunked+deflated at the requested level when compression is enabled. This is a closed-form predicate on `(array byte size, UseCompression, CompressionLevel)` — tests assert it directly via `UnitTest::ProbeHdf5Dataset` rather than trusting the filter's own claim about what it wrote. -- **Pipeline embedding:** when run inside an actual `Pipeline`, the written file's embedded pipeline JSON must reproduce the exact filter sequence and count that was executed — asserted against the hand-built pipeline (`pipeline.size() == 3`, filter names checked by index). +*Applied:* Constructed values define expected arrays and geometry links; HDF5 probes check compression, and XDMF text checks require the output filename in data references. -**Encoded tests:** `src/Plugins/SimplnxCore/test/DREAM3DFileTest.cpp` (20 of the file's 23 `TEST_CASE`s touch Write, several parameterized via `GENERATE`/`DYNAMIC_SECTION`) — all pass. See Test inventory below for the full list and for the three `ReadDREAM3DFilter`-only cases that are deliberately out of scope. +*Encoded:* `src/Plugins/SimplnxCore/test/DREAM3DFileTest.cpp::WriteDREAM3DFilter:Valid Parameters`, [File I/O test](../test/DREAM3DFileTest.cpp#L1318), and the five `WriteDREAM3DFilter: Compression_*` cases provide seven content, layout and validation fixtures. All pass in CI. -*Second-engineer review:* **Signed off by Michael A. Jackson , 2026-08-20** (PR #1683 review, per sign-off convention). The V&V work was authored by Matthew Marine, so the review is independent of the author. Reviewed across three passes (2026-07-23, 2026-08-12, and this closing pass): - -- **Oracle design.** Confirmed Class 1 is correct and non-circular: every fixture builds its own `DataStructure` in C++ and asserts the read-back against the hand-known fill pattern, so no previously-captured file is ever the source of truth. The HDF5 physical-layout assertions are a genuine closed-form predicate on `(byte size, UseCompression, CompressionLevel)` checked via `ProbeHdf5Dataset`, not a restatement of what the filter reported writing. -- **Class 4 companion.** The file-size monotonicity invariant in `Compression_LevelsRoundTrip` is a legitimate invariant check, not a substitute for the per-level content round-trip that runs alongside it. -- **Scope boundary (D2).** Confirmed by inspection that `StatsDataArray`/`StructArray` do not exist in this branch, so the exclusion is a real scope boundary rather than an untested path. -- **The 4 uncovered defensive paths.** Each was independently confirmed unreachable through the public API, and each appears as its own row in the code-path table rather than being omitted. -- **Bugs found during review.** Three transposed `std::string_view` arguments in `WriteXdmfNodeGeometry1D/2D/3D` and a wrong-`Result` test in `DREAM3D::ReadFile` are resolved; both are recorded under Bug flags with the tests that now pin them. +*Second-engineer review:* Michael Jackson, 2026-08-20. ## 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. - -## Code path coverage - -**14 of 19** paths exercised. The 5 gaps are all defensive guards that require conditions unreachable through the public filter/pipeline API (invalid destination mid-write after preflight already validated it, or a detached `PipelineFilter`) rather than genuine untested behavior. +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 `WriteDREAM3DFilter-D3`. DREAM3D-NX v7.4.2 contains the fix for `WriteDREAM3DFilter-D4`. -Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/WriteDREAM3D.cpp` (86 lines). Preflight guards below live in the sibling `Filters/WriteDREAM3DFilter.cpp` (`preflightImpl`), which the policy still treats as in-scope algorithm surface (parameter validation gates that the Algorithm class depends on). +| Deviation | Defect | Affected released versions | Resolution | +|-----------|--------|----------------------------|------------| +| `WriteDREAM3DFilter-D3` | XDMF node-geometry helpers exchanged the geometry name and HDF5 filename, which broke attribute references. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | Each helper forwards the geometry name and filename in the correct order. | +| `WriteDREAM3DFilter-D4` | The shared `ReadFile` utility checked the pipeline result twice and could access a failed DataStructure result. | DREAM3D-NX v7.0.0 through v7.4.1. DREAM.3D v6.5.171 was not affected. | The reader returns DataStructure import errors before it accesses the value. | -| # | Phase | Path | Test case | -|----|--------------------|----------------------------------------------------------------------------------------------|-----------| -| 1 | Preflight | `export_file_path` empty → error `-1` | `"WriteDREAM3DFilter:Invalid Parameters"` § `Empty FilePath` | -| 2 | Preflight | `use_compression=true`, `compression_level < 1` → error `-2` | `"WriteDREAM3DFilter:Invalid Parameters"` § `Bad Compression Level`; `"WriteDREAM3DFilter: Compression_Preflight_RejectsOutOfRangeLevel"` (level=0) | -| 3 | Preflight | `use_compression=true`, `compression_level > 9` → error `-2` | `"WriteDREAM3DFilter: Compression_Preflight_RejectsOutOfRangeLevel"` (level=10) | -| 4 | Preflight | `use_compression=false` → `compression_level` ignored even if out of `[1,9]` | `"WriteDREAM3DFilter: Compression_Preflight_RejectsOutOfRangeLevel"` (level=0, compression off) | -| 5 | Preflight | Valid parameters → success | `"WriteDREAM3DFilter:Valid Parameters"`; implicitly, every passing execute-path test below | -| 6 | Execute — setup | `AtomicFile::Create` fails (unwritable/invalid destination directory) | *Not directly tested.* Preflight only rejects an empty path; a directory-permission failure at execute time would require a filesystem fixture (e.g., a read-only directory) not set up by the current suite. | -| 7 | Execute — setup | `AtomicFile::Create` succeeds | Every passing execute-path test | -| 8 | Execute — pipeline | `PipelineNode != nullptr` and `getPrecedingPipeline()` returns `nullptr` → error `-15` | *Not directly tested.* Only reachable if a `PipelineFilter` is detached from its parent `Pipeline`, which normal `filter.execute()`/`pipeline.execute()` usage never produces. | -| 9 | Execute — pipeline | `PipelineNode != nullptr`, preceding pipeline retrieved successfully → embedded in file | `"DREAM3DFileTest:Import/Export DREAM3D Filter Test"` (`exportPipeline.execute()`); `"DREAM3DFileTest:Import/Export Multi-DREAM3D Filter Test"` (`CreateMultiExportFiles()`) | -| 10 | Execute — pipeline | `PipelineNode == nullptr` → empty pipeline written | `"WriteDREAM3DFilter:Valid Parameters"`, `"DREAM3DFileTest::StringArray"`, all `Compression_*` tests (all call `filter.execute(ds, args)` directly) | -| 11 | Execute — options | `use_compression=true` → `writeOptions.compressionLevel = CompressionLevel` | `"...Compression_On_IsChunkedAndDeflated"`, `"...Compression_SmallArray_Bypasses"`, `"...Compression_LevelsRoundTrip"` | -| 12 | Execute — options | `use_compression=false` → `writeOptions.compressionLevel = 0` | `"...Compression_Off_IsContiguous"`; `"WriteDREAM3DFilter:Valid Parameters"` | -| 13 | Execute — write | `DREAM3D::WriteFile(...)` returns invalid → skip commit, return the error | `"WriteDREAM3DFilter:Unwritable DataObject Type"` — writes a `DataStructure` holding a `GridMontage`, a type with no registered HDF5 IO factory. Asserts preflight still succeeds, execute fails with exactly one error of code `-5` ("Could not find IO factory for datatype: …"), and the destination file does **not** exist afterward (i.e. the `AtomicFile` was never committed). See the capability-boundary note at the end of this section. | -| 14 | Execute — write | `DREAM3D::WriteFile(...)` returns valid → proceed to commit | Every passing execute-path test | -| 15 | Execute — commit | `atomicFile.commit()` fails (rename onto final destination fails) | *Not directly tested.* Would require the destination path to become invalid between `AtomicFile::Create` and `commit()` (e.g., concurrent deletion of the parent directory) — a race not exercised by the suite. | -| 16 | Execute — commit | `atomicFile.commit()` succeeds | Every passing execute-path test (the output file is present and re-readable in every round-trip test) | -| 17 | Execute — xdmf | `write_xdmf_file=true` → rename temp `.xdmf` into place, succeeds | `"DREAM3DFileTest:DREAM3D File IO Test"` (writeXdmf=true), `CreateExportPipeline()`/`CreateMultiExportFiles()` (`write_xdmf_file=true`) | -| 18 | Execute — xdmf | `write_xdmf_file=true`, rename fails → `MakeErrorResult` with system error message | *Not directly tested.* Would require the `.xdmf` destination to become unwritable between the HDF5 write succeeding and the rename — not portably reproducible in the current suite. | -| 19 | Execute — xdmf | `write_xdmf_file=false` → skip rename, return `WriteFile`'s result directly | Most `Compression_*` tests, `"DREAM3DFileTest::StringArray"`, `"WriteDREAM3DFilter:Valid Parameters"` | -| 20 | OOC storage | chunk-tail raw-read oracle | `real HDF5 chunk-tail raw-read oracle` — independent exact values across the boundary | - - -**Capability boundary behind Path 13 — `DataObject` types the shared HDF5 IO layer cannot write.** - -`HDF5::DataIOManager::addCoreFactories()` (`DataStructure/IO/HDF5/DataIOManager.cpp:33-81`) registers factories per concrete `DataArray` type, per `NeighborList` specialization, per `ScalarData` type, the eight geometries, `AttributeMatrix`, `DataGroup`, and `StringArray`. Two `DataObject` types reachable in the current codebase are **not** registered, and any `DataStructure` containing one cannot be written: `HDF5::DataStructureWriter::WriteFile` hits its "no factory found" guard (`DataStructureWriter.cpp:153-157`) and fails with error `-5` ("Could not find IO factory for datatype: …"), surfacing through `WriteDREAM3DFilter` as Path 13 above. - -1. **`DynamicListArray`** — the generic variable-length per-tuple list container that `NeighborList` is itself built on. It has no IO class at all in `DataStructure/IO/HDF5/`, so nothing could be registered. Only reachable by constructing one directly; no shipped filter creates a bare `DynamicListArray` outside of a `NeighborList` specialization. - -2. **`GridMontage`** — unlike the above, this type *does* have an IO class (`DataStructure/IO/HDF5/GridMontageIO.cpp`, with both `readData` and `writeData` bodies), but that class is never passed to `addFactory<>()`, so it is unreachable and the type is unwritable in practice. `GridMontage` is more reachable than `DynamicListArray`: `CreateGridMontageAction` (`src/simplnx/Filter/Actions/CreateGridMontageAction.cpp`) is public core API that any filter may use to create one. No filter in this repository currently does, so no shipped pipeline can hit this today — but a plugin filter using that public Action would produce a `DataStructure` that `WriteDREAM3DFilter` cannot save. - - **This is recorded as an observation, not as a defect with a known fix.** Montage support in SIMPLNX is an open design question that has deliberately not been settled yet, and the existing `GridMontage`/`GridMontageIO`/`CreateGridMontageAction` code predates that decision — it should be treated as an unfinished sketch inherited from the legacy SIMPL montage design, not as a foundation that merely needs switching on. Simply calling `addFactory()` would *not* be a correct fix: `GridMontageIO::writeData` already creates a `groupWriter` it never uses and then passes `parentGroup` to `WriteBaseGroupData`, so registering it as-is would write montage contents into the wrong group. How montages should be represented on disk (and whether `GridMontage` is even the right in-memory abstraction) needs to be designed before any of this code is revived. This V&V cycle's only claim is the one its test makes: **today**, a `DataStructure` containing a `GridMontage` fails to write with error `-5` and leaves no partial file behind. +## Code path coverage -Both are gaps in the shared HDF5 IO layer, not in `WriteDREAM3DFilter`'s own algorithm — the same gaps affect `ReadDREAM3DFilter` for the same types. Neither is raised as a formal Deviation because there is no confirmed 6.5.171 pipeline behavior being compared against (unlike D2, which names concrete legacy types); they are recorded here as known capability boundaries of what this filter can serialize. The failure mode itself is no longer untested: `"WriteDREAM3DFilter:Unwritable DataObject Type"` pins it via `GridMontage`, asserting the `-5` error and that no partial file is committed. +15 of 21 paths exercised. The table distinguishes ordinary-test coverage from failure paths and the hidden storage-boundary fixture. + +Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/WriteDREAM3D.cpp` (86 lines) and `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteDREAM3DFilter.cpp` (142 lines). Shared I/O checks use `src/simplnx/Utilities/Parsing/DREAM3D/Dream3dIO.cpp` (3302 lines). + +| # | Phase | Path | Test case | +|---|-------|------|-----------| +| 1 | Preflight | Empty output path is rejected | `WriteDREAM3DFilter:Invalid Parameters` — `Empty FilePath` checks an invalid result, not a specific error code | +| 2 | Preflight | Compression enabled, level below 1 → error `-2` | `WriteDREAM3DFilter:Invalid Parameters`; `WriteDREAM3DFilter: Compression_Preflight_RejectsOutOfRangeLevel` | +| 3 | Preflight | Compression enabled, level above 9 → error `-2` | `WriteDREAM3DFilter: Compression_Preflight_RejectsOutOfRangeLevel` | +| 4 | Preflight | Compression disabled → ignore an out-of-range level | `WriteDREAM3DFilter: Compression_Preflight_RejectsOutOfRangeLevel` | +| 5 | Preflight | Valid parameters → success | `WriteDREAM3DFilter:Valid Parameters` | +| 6 | Setup | `AtomicFile::Create` fails → return its error | *Not directly tested. No fixture injects a destination or permission failure at creation.* | +| 7 | Setup | `AtomicFile::Create` succeeds | `WriteDREAM3DFilter:Valid Parameters` | +| 8 | Pipeline | A pipeline node has no preceding pipeline → error `-15` | *Not directly tested. No fixture supplies a detached pipeline node.* | +| 9 | Pipeline | A pipeline node supplies its preceding pipeline | [Single-file import/export test](../test/DREAM3DFileTest.cpp#L1402); [Multiple-file import/export test](../test/DREAM3DFileTest.cpp#L1513) | +| 10 | Pipeline | No pipeline node → write an empty pipeline | `WriteDREAM3DFilter:Valid Parameters` — combined reader accepts the saved pipeline and data | +| 11 | Compression | Enabled → use the selected compression level | `WriteDREAM3DFilter: Compression_On_IsChunkedAndDeflated`; `WriteDREAM3DFilter: Compression_SmallArray_Bypasses`; `WriteDREAM3DFilter: Compression_LevelsRoundTrip` | +| 12 | Compression | Disabled → set compression level to zero | `WriteDREAM3DFilter: Compression_Off_IsContiguous` | +| 13 | Write | `WriteFile` fails → return its error without file replacement | `WriteDREAM3DFilter:Unwritable DataObject Type` — error `-5`, no destination file | +| 14 | Write | `WriteFile` succeeds | `WriteDREAM3DFilter:Valid Parameters` — expected data values survive the round trip | +| 15 | File replacement | Atomic file replacement fails → return its error | *Not directly tested. No fixture injects a failure after the temporary file is written.* | +| 16 | File replacement | Atomic file replacement succeeds | `WriteDREAM3DFilter:Valid Parameters` — destination exists and can be read | +| 17 | XDMF | Enabled → rename the sidecar successfully | [Single-file import/export test](../test/DREAM3DFileTest.cpp#L1402) exercises the rename; [Pipeline and XDMF combinations test](../test/DREAM3DFileTest.cpp#L1210) checks references from the shared writer | +| 18 | XDMF | Sidecar rename fails → return the system error | *Not directly tested. No fixture injects a sidecar rename failure.* | +| 19 | XDMF | Disabled → skip sidecar rename | `WriteDREAM3DFilter:Valid Parameters`; `WriteDREAM3DFilter: Compression_Off_IsContiguous` | +| 20 | Storage boundary | Write a full source chunk and one extra tuple | *Not directly tested. The dedicated `SimplnxCore::WriteDREAM3DFilter: real HDF5 chunk-tail raw-read oracle` is hidden and requires the HDF5-OOC manager.* | +| 21 | Cancellation | Cancellation after a successful write → return `-1` before file replacement | *Not directly tested. No fixture sets the cancellation flag at this point.* | ## Test inventory +`src/Plugins/SimplnxCore/test/DREAM3DFileTest.cpp` contains all 24 cases below. The 23 ordinary cases run in the in-core configuration. All pass in CI. Linked test labels point to the exact declarations in this file. + | Test case | Status | Notes | |-----------|--------|-------| -| `WriteDREAM3DFilter:Invalid Parameters` (§ Empty FilePath, § Bad Compression Level) | kept | Preflight-only, empty `DataStructure` (the guards never touch data). Covers Paths 1, 2. | -| `WriteDREAM3DFilter:Valid Parameters` | kept | Preflight + full execute, full `CreateTestDataStructure()` fixture, compression off, own output file. Covers Paths 5, 7, 10, 12, 14, 16, 19. | -| `WriteDREAM3D:Pipeline / WriteXdmf combinations` | kept | Calls `DREAM3D::WriteFile(path, ds, pipeline, writeXdmf)` directly (bypasses `AtomicFile`/the Algorithm class) across `writeXdmf × {real pipeline, empty pipeline}` (4 cases), own output file. Exercises the shared write utility the Algorithm depends on, not the Algorithm's own guards; validates the `.xdmf` sidecar's heavy-data references via `CheckXdmfFile`. | -| `WriteDREAM3D:FileData Overload` | new-for-V&V | Writes through the exported `DREAM3D::WriteFile(HDF5::FileIO&, const FileData&)` forwarder (via `CreateFileData()`) and round-trips the full fixture — keeps the overload's pipeline/DataStructure argument ordering covered. | -| `WriteDREAM3D:Invalid File` | kept | Same free-function call with an empty path (4 cases via `GENERATE`); asserts the underlying `HDF5::FileIO::WriteFile` failure is surfaced, not `AtomicFile`'s. No sidecar check — a failed write has no target whose sidecar could exist. | -| `WriteDREAM3DFilter:Unwritable DataObject Type` | new-for-V&V | Full filter execute on a `DataStructure` containing a `GridMontage` (no registered IO factory). Pins the write-failure contract end to end: preflight valid, execute invalid with exactly one error of code `-5`, and no file left at the destination — the only test covering Path 13 and the `AtomicFile`-not-committed guarantee. GridMontage is only the vehicle for reaching that path, not the subject of the test (see the capability-boundary note under Code path coverage). | -| `DREAM3DFileTest:DREAM3D File IO Test` | kept | The primary Class 1 content-fidelity test. Builds every geometry/`DataObject` type (`CreateTestDataStructure`), writes + reads back (`writeXdmf ∈ {true,false}`), and asserts full structural/content equality (`CheckTestDataStructure`) plus pipeline round-trip (`pipeline.size()==3`, filter names by index). | -| `DREAM3DFileTest::StringArray` | kept | Round-trips a `StringArray` through the actual `WriteDREAM3DFilter`/`ReadDREAM3DFilter` classes (not the free function) — covers Path 10 with real filter execution. | -| `DREAM3DFileTest:Import/Export DREAM3D Filter Test` | kept | Executes `WriteDREAM3DFilter` inside a real `Pipeline` (`exportPipeline.execute()`) — the only test exercising Path 9 (non-null `PipelineNode` with a real preceding pipeline). Also checks preflight-imported vs. executed array store types on the read side. | -| `DREAM3DFileTest:Import/Export Multi-DREAM3D Filter Test` | kept | Two independent export pipelines (`CreateMultiExportFiles`) each executing `WriteDREAM3DFilter` in-pipeline, then a single import pipeline consuming both files. Second confirmation of Path 9. | -| `DREAM3DFileTest: Preflight imports geometry connectivity as metadata-only stores` | kept | Read-side only (consumes a pre-existing `geoms.dream3d` asset); does not exercise `WriteDREAM3DFilter`. Listed for completeness since it shares the tag set. | -| `SimplnxCore::WriteDREAM3DFilter: SIMPL Backwards Compatibility` | kept | `DYNAMIC_SECTION` over SIMPL 6.5 (UUID-keyed) and 6.4 (name-keyed) `DataContainerWriter` fixtures. Asserts UUID resolution, empty comments, and `export_file_path`/`write_xdmf_file` value conversion. Not an oracle test — argument-mapping check only. | -| `DREAM3DFileTest: DataArray datasets are chunked+deflated when WriteOptions requests it` | kept | Calls the `WriteOptions`-aware free function directly with a 2 MB array; asserts chunked+deflate layout at level 5 via `ProbeHdf5Dataset`, then round-trips content. Covers the compression-options contract the Algorithm class relies on. | -| `WriteDREAM3DFilter: Compression_Off_IsContiguous` | kept | Full filter execute, `use_compression=false`. Asserts contiguous/no-deflate layout. Covers Path 12. | -| `WriteDREAM3DFilter: Compression_On_IsChunkedAndDeflated` | kept | Full filter execute, `use_compression=true`, level 5, 500K-element array. Asserts chunked+deflate layout and round-trips content. Covers Path 11. | -| `WriteDREAM3DFilter: Compression_SmallArray_Bypasses` | kept | Full filter execute with one <16 KiB and one >16 KiB array in the same `DataStructure`, `use_compression=true`. Asserts the small array stays contiguous/uncompressed while the large one is chunked+deflated — the Class 1 closed-form threshold check. | -| `WriteDREAM3DFilter: Compression_LevelsRoundTrip` | kept | Full filter execute at levels {1,5,9} on the same 1M-element pattern. Round-trips content at each level and asserts non-increasing file size as level rises (Class 4 companion invariant). | -| `WriteDREAM3DFilter: Compression_Preflight_RejectsOutOfRangeLevel` | kept | Three sequential preflight-only checks: level=0 (invalid), level=10 (invalid), level=0 with compression off (valid — ignored). Covers Paths 2, 3, 4. | -| `DREAM3DFileTest: PreflightCache avoids re-reading unchanged files` | kept | Uses `DREAM3D::WriteFile` only to create read-side fixture files; does not exercise `WriteDREAM3DFilter`'s own behavior. Listed for completeness since it shares source-file/tag space. | -| `DREAM3DFileTest: Geometry Nested In DataGroup Round Trip` | kept | Executes `WriteDREAM3DFilter` directly across all 8 geometry types, each at top level and nested inside a `DataGroup` (issue #1642 regression coverage), `write_xdmf_file=false`. Covers Paths 7, 10, 14, 16, 19 with per-geometry fixtures. | -| `real HDF5 chunk-tail raw-read oracle` | new-for-V&V | 27 assertions; The numeric source is HDF5-OOC with 65,536 four-component uint32 tuples per 1 MiB chunk and one extra tuple. WriteDREAM3DFilter runs with compression disabled/enabled. Direct HDF5 dataset reads, without a DataStructure round trip, match all 262,148 independently constructed integer values. The physical deflate flag also matches the requested compression option. The 22-entry CTest selection includes shared DREAM3D file-I/O cases. | - -**Deliberately out of scope (3 of the file's 23 `TEST_CASE`s).** `DREAM3DFileTest.cpp` is shared between the read and write sides of DREAM3D file IO. These three exercise `ReadDREAM3DFilter` only and belong to its V&V, not this one: `DREAM3DFileTest: Existing Data Objects Test` (importing into a populated `DataStructure`), `DREAM3DFileTest: Path Import Policy Tests` (read-side path-collision policy), and `SimplnxCore::ReadDREAM3DFilter: SIMPL Backwards Compatibility` (read-side SIMPL argument conversion). They are named here rather than silently omitted so the exclusion can be audited. - -**Dual-build verification at sign-off:** the DREAM3D file IO tests pass **32/32 in both** the in-core (`NX-Com-Qt69-Vtk96-Rel`) and out-of-core (`NX-OOC-Qt69-Vtk95-Rel`) Release builds, at the rebased head. The full `SimplnxCore::` suite also passes 979/979 in-core. - -OOC recertification (2026-09-21): 22/22 entries pass in both DREAM3DNX CTest selections. The hidden `real HDF5 chunk-tail raw-read oracle` passes 27 assertions. The numeric source is HDF5-OOC with 65,536 four-component uint32 tuples per 1 MiB chunk and one extra tuple. WriteDREAM3DFilter runs with compression disabled/enabled. Direct HDF5 dataset reads, without a DataStructure round trip, match all 262,148 independently constructed integer values. The physical deflate flag also matches the requested compression option. The 22-entry CTest selection includes shared DREAM3D file-I/O cases. Upstream oracle expectations are retained. This campaign changes no production algorithms or historical sign-offs and claims no new legacy binary comparison. +| `WriteDREAM3DFilter:Invalid Parameters` | kept | Rejects an empty output path and compression level 0 when compression is enabled. | +| `SimplnxCore::WriteDREAM3DFilter: real HDF5 chunk-tail raw-read oracle` | new-for-V&V | Hidden. Direct HDF5 reads compare 262,148 uint32 values across a 65,536-tuple boundary, with compression off and on; checks the deflate flag. | +| `WriteDREAM3DFilter:Valid Parameters` | kept | Executes the filter and checks constructed values, geometry links and tuple shapes after reading the result. | +| [Pipeline and XDMF combinations test](../test/DREAM3DFileTest.cpp#L1210) | kept | Calls the shared writer for four pipeline/sidecar combinations. Checks XDMF filename prefixes when enabled. | +| [FileData overload test](../test/DREAM3DFileTest.cpp#L1231) | new-for-V&V | Calls the FileData overload and checks pipeline size, constructed values and tuple shapes after reading. | +| [Invalid-file test](../test/DREAM3DFileTest.cpp#L1256) | kept | Checks failure for an empty path in all four pipeline/sidecar combinations. | +| `WriteDREAM3DFilter:Unwritable DataObject Type` | new-for-V&V | A GridMontage has no registered HDF5 factory. Checks error `-5` and no destination file. | +| [File I/O test](../test/DREAM3DFileTest.cpp#L1318) | kept | Checks arrays, scalar values, eight geometry types and links, pipeline size and filter names; validates XDMF filename prefixes. | +| `DREAM3DFileTest::StringArray` | kept | Writes and reads `foo`, `bar`, `baz` through the filters and compares every string. | +| [Single-file import/export test](../test/DREAM3DFileTest.cpp#L1402) | kept | Executes an export pipeline; checks imported objects and the difference between execution stores and metadata-only preflight stores. | +| `DREAM3DFileTest: Preflight imports geometry connectivity as metadata-only stores` | kept | Reader check on `geoms.dream3d`: ten connectivity/bounds paths in each of the `Preflight`, `Execute` and `DataStructureReader::ReadFile` dynamic-section groups. Checks store type and tuple count. | +| [Multiple-file import/export test](../test/DREAM3DFileTest.cpp#L1513) | kept | Imports two exported files; checks both top-level groups and tuple shapes. | +| `DREAM3DFileTest: Existing Data Objects Test` | kept | Reader check: imports Small IN100 into a populated DataStructure and checks tuple shapes. | +| `DREAM3DFileTest: Path Import Policy Tests` | kept | Reader check: imports all paths or includes/excludes selected leaves and parents from Small IN100. | +| `SimplnxCore::ReadDREAM3DFilter: SIMPL Backwards Compatibility` | kept | Reader conversion check in `SIMPL 6.5 (UUID)` and `SIMPL 6.4 (Filter_Name)` dynamic sections; checks filter identity and pipeline size. | +| `SimplnxCore::WriteDREAM3DFilter: SIMPL Backwards Compatibility` | kept | The same two conversion sections check UUID, comments, output path, XDMF setting and disabled compression. | +| `DREAM3DFileTest: DataArray datasets are chunked+deflated when WriteOptions requests it` | kept | Shared writer: 500,000 float32 values, level 5 deflate, chunked layout and exact array round trip. | +| `WriteDREAM3DFilter: Compression_Off_IsContiguous` | kept | Checks contiguous layout and no deflate for 200,000 float32 values. | +| `WriteDREAM3DFilter: Compression_On_IsChunkedAndDeflated` | kept | Checks chunked layout, level 5 deflate and an exact round trip of 500,000 float32 values. | +| `WriteDREAM3DFilter: Compression_SmallArray_Bypasses` | kept | A 400-byte array stays contiguous; a 2 MB array uses chunking and deflate. | +| `WriteDREAM3DFilter: Compression_LevelsRoundTrip` | kept | Checks one million repeating values at levels 1, 5 and 9, then requires non-increasing file size for this fixture. | +| `DREAM3DFileTest: Geometry Nested In DataGroup Round Trip` | kept | Eight dynamic sections: VertexGeom, EdgeGeom, TriangleGeom, QuadGeom, TetrahedralGeom, HexahedralGeom, ImageGeom and RectGridGeom. Checks concrete types at top level and inside a group. | +| `WriteDREAM3DFilter: Compression_Preflight_RejectsOutOfRangeLevel` | kept | Rejects levels 0 and 10 with compression enabled; accepts level 0 with compression disabled. | +| `DREAM3DFileTest: PreflightCache avoids re-reading unchanged files` | kept | Reader cache check: repeated preflight adds no miss or file open; execution reads values, and file replacement invalidates the cache. | ## Exemplar archive -None. Every test above builds its input `DataStructure` inline in C++ and never loads a downloaded `.tar.gz` exemplar — the Class 1 oracle's "expected output" is the test's own hand-built input, so no cached golden file is required or used. (`Small_IN100_dream3d_v3.tar.gz`, referenced elsewhere in this test file, backs unrelated `ReadDREAM3DFilter`-only test cases and is not consumed by any Write-side test.) - -## Deviations from DREAM3D 6.5.171 +- **Archive:** None for writer verification. The two Small IN100 reader cases use `Small_IN100_dream3d_v3.tar.gz`; writer expectations are constructed in C++. +- **SHA512:** No writer archive. The shared reader archive matches `test/CMakeLists.txt`: `bfa9547e787b0f8e8122702da0eb4e519f48a48bf4f94aad020f72479d071d32dfc96a1425705874c68507a61ed391d28606d9c4f4acd559043ef0ace64fd33f`. +- **Provenance:** Not applicable to the constructed writer fixtures; their builders and expected values are in `src/Plugins/SimplnxCore/test/DREAM3DFileTest.cpp`. -No byte/dataset-level A/B was run against 6.5.171 `DataContainerWriter` output, and none is warranted: the two writers target deliberately different on-disk contracts (see D1), so such a diff would be 100% noise by design, not signal. `ReadDREAM3DFilter` is the only tool that understands both formats, and cross-format fidelity is its concern, not this filter's. Two scope/format deviations are documented instead: +## Deviations from DREAM.3D v6.5.171 -- `WriteDREAM3DFilter-D1` — on-disk file format is a deliberate, complete rewrite (v8 `DataStructure` layout vs. legacy v7 `DataContainers` layout; optional gzip compression; atomic write) — see `vv/deviations/WriteDREAM3DFilter.md`. -- `WriteDREAM3DFilter-D2` — `StatsDataArray`/`StructArray` (SIMPL ensemble-statistics types) cannot currently be written because those `DataObject` types do not yet exist in this branch of simplnx — see `vv/deviations/WriteDREAM3DFilter.md`. +See `vv/deviations/WriteDREAM3DFilter.md` for the root cause, affected users and recommendation. These Part 1 differences follow from source inspection. -Neither is a bug: D1 is the intended outcome of the Rewrite classification (defended above), and D2 is a scope boundary pending a separate in-progress port of those data types. +| Deviation | Observed difference | +|-----------|---------------------| +| `WriteDREAM3DFilter-D1` | SIMPLNX writes version 8 `DataStructure` files instead of version 7 `DataContainers` files. | +| `WriteDREAM3DFilter-D2` | SIMPLNX imports legacy statistics as groups and arrays; it does not write legacy `StatsDataArray` or `StructArray` objects. | diff --git a/src/Plugins/SimplnxCore/vv/deviations/WriteDREAM3DFilter.md b/src/Plugins/SimplnxCore/vv/deviations/WriteDREAM3DFilter.md index e280424a14..e8a3fa9407 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/WriteDREAM3DFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/WriteDREAM3DFilter.md @@ -1,12 +1,12 @@ -# Deviations from DREAM3D 6.5.171: WriteDREAM3DFilter +# Deviations from DREAM.3D v6.5.171: WriteDREAM3DFilter -This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent (`DataContainerWriter`). - -Entries are referenced by stable ID (`WriteDREAM3DFilter-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 file-format and data-representation differences from `DataContainerWriter` in DREAM.3D v6.5.171. Part 2 lists shared-I/O defects found and fixed during this filter's V&V. Both parts use the stable `WriteDREAM3DFilter-D` sequence. --- -## WriteDREAM3DFilter-D1 +## Part 1 — Differences from DREAM.3D v6.5.171 + +### WriteDREAM3DFilter-D1 | Field | Value | |---|---| @@ -14,19 +14,19 @@ Entries are referenced by stable ID (`WriteDREAM3DFilter-D`) from the V&V rep | **Filter UUID** | `b3a95784-2ced-41ec-8d3d-0242ac130003` | | **Status** | active | -**Symptom:** A `.dream3d` file written by SIMPLNX is not byte-compatible with, and cannot be opened by, a DREAM3D 6.5.171 install expecting the legacy layout — and the reverse is only possible through SIMPLNX's dedicated legacy-import code path (`ReadDREAM3DFilter`'s legacy `DataContainers` reader), not by treating the file as interchangeable. +**Symptom:** A SIMPLNX `.dream3d` file uses a different HDF5 layout from a DREAM.3D v6.5.171 file and cannot serve as a legacy-format replacement. -**Root cause:** Algorithmic choice. SIMPLNX writes a clean-sheet v8 HDF5 layout: a top-level `DataStructure` group (vs. legacy's `DataContainers` group), a `FileVersion` attribute of `"8.0"` (vs. legacy `"7.0"`), an embedded JSON pipeline representation (vs. legacy's own pipeline serialization), atomic-rename write semantics (`AtomicFile`, so a crash mid-write cannot leave a corrupt file at the destination path), and an optional gzip/deflate compression scheme for `DataArray`/`NeighborList` datasets that has no legacy equivalent. This has been true since the filter's introduction — it was never a translation of legacy `DataContainerWriter`'s C++ (see Algorithm Relationship in the V&V report). +**Root cause:** Algorithmic choice. SIMPLNX writes file version 8.0 with a `DataStructure` group. DREAM.3D v6.5.171 writes version 7.0 with a `DataContainers` group. The new writer supports nested data objects, optional gzip compression and atomic replacement of the HDF5 destination. The legacy UUID mapping preserves the save-data role and converts the output-path and XDMF parameters; it does not promise the same file layout. Converted SIMPL pipelines disable compression, and the legacy time-series option has no corresponding writer parameter. -**Affected users:** Anyone attempting to open a SIMPLNX-written `.dream3d` file directly in a DREAM3D 6.5.171 install, or vice versa, outside of DREAM3D-NX's own dual-format `ReadDREAM3DFilter`. Users staying entirely within DREAM3D-NX (write with `WriteDREAM3DFilter`, read with `ReadDREAM3DFilter`) never observe this — round-trip fidelity within the new format is verified by the Class 1 tests in the V&V report. +**Affected users:** Users who exchange `.dream3d` files with DREAM.3D v6.5.171 or read the HDF5 layout directly. -**Why no A/B comparison was attempted:** Because the two writers deliberately target different on-disk contracts, a byte-level or dataset-level A/B diff between a SIMPLNX-written file and a 6.5.171-written file would not measure anything meaningful — every dataset path, group name, and file-version tag differs by construction, so the diff would be 100% noise, not signal. This is why the V&V report's "Legacy comparison" is marked **Not run — and not applicable** rather than "deferred." `ReadDREAM3DFilter` is the only tool in either codebase that understands both the legacy `DataContainers` layout and the current `DataStructure` layout; it is the correct place to verify cross-format fidelity (import old → re-export new → confirm no data loss), not a Write-side A/B. +**Recommendation:** Trust SIMPLNX for the new file format. Use `ReadDREAM3DFilter` to import legacy files into DREAM3D-NX, and update external readers to the version 8 layout. -**Recommendation:** Trust SIMPLNX. The new format is a deliberate design improvement (atomicity, compression, JSON pipeline embedding) required for capabilities legacy DREAM3D never had (out-of-core datasets, versioned pipeline metadata). It is not "wrong" relative to 6.5.171; it is a different, and newer, on-disk contract. DREAM3D-NX remains able to *read* legacy 6.5.171 files via `ReadDREAM3DFilter`'s legacy import path, so migration is one-directional by design (import old, export new) rather than bidirectional. +**Evidence:** `src/simplnx/Utilities/Parsing/DREAM3D/Dream3dIO.hpp` defines versions 7.0 and 8.0. `WriteDREAM3DFilter::FromSIMPLJson()` defines the parameter conversion. SIMPL source `0afaa6854` (the last revision before the DREAM.3D v6.5.171 tag date) defines version 7.0 and `DataContainers` in `Source/SIMPLib/Common/Constants.h`; `Source/SIMPLib/CoreFilters/DataContainerWriter.cpp` writes that layout. The round-trip tests compare constructed values, not bytes from a legacy file. --- -## WriteDREAM3DFilter-D2 +### WriteDREAM3DFilter-D2 | Field | Value | |---|---| @@ -34,10 +34,58 @@ Entries are referenced by stable ID (`WriteDREAM3DFilter-D`) from the V&V rep | **Filter UUID** | `b3a95784-2ced-41ec-8d3d-0242ac130003` | | **Status** | active | -**Symptom:** A pipeline that would have produced `StatsDataArray` or `StructArray` objects in legacy DREAM3D (e.g., ensemble statistics from a "Generate Ensemble Statistics"-style filter) cannot have those objects written to a `.dream3d` file by `WriteDREAM3DFilter` in the current develop branch — there is nothing in the `DataStructure` for the writer to serialize, because the types themselves do not exist yet. +**Symptom:** Imported legacy statistics use groups and arrays in SIMPLNX instead of native `StatsDataArray` and `StructArray` objects. + +**Root cause:** Library. SIMPLNX has no `StatsDataArray` or `StructArray` DataObject type. Its legacy reader recognizes the `Statistics` object type, creates a `Statistics` DataGroup and recursively imports supported child groups, datasets and attributes. The writer has HDF5 factories for the resulting groups and arrays. The absence of the legacy C++ types does not mean that statistics must be absent from exported files. + +**Affected users:** Users who migrate legacy statistics or use software that expects the legacy statistics object types and schema. + +**Recommendation:** Trust SIMPLNX for saving its supported groups and arrays. Adapt consumers to the imported representation instead of expecting legacy object types or a legacy-format export. + +**Evidence:** `src/simplnx/Utilities/Parsing/DREAM3D/Dream3dIO.cpp` implements `readLegacyStatsDataArray()` and `readLegacyStatsDataArrayChild()`. `src/simplnx/DataStructure/IO/HDF5/DataIOManager.cpp` registers DataGroup, numeric-array and StringArray factories. SIMPL source `0afaa6854` contains `Source/SIMPLib/DataArrays/StatsDataArray.cpp` and `StructArray.hpp`. The writer test file has no statistics-specific import/export fixture. + +--- + +## Part 2 — SIMPLNX defects and behavior changes + +### WriteDREAM3DFilter-D3 + +| Field | Value | +|---|---| +| **Deviation ID** | `WriteDREAM3DFilter-D3` | +| **Filter UUID** | `b3a95784-2ced-41ec-8d3d-0242ac130003` | +| **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:** XDMF attribute references on node geometries could contain the geometry name where the HDF5 filename was required. + +**Root cause:** Bug. `WriteXdmfNodeGeometry1D`, `WriteXdmfNodeGeometry2D` and `WriteXdmfNodeGeometry3D` exchanged two `std::string_view` arguments when they called the lower-dimensional helper. The compiler accepted the swapped arguments. The helpers now preserve the geometry-name and filename order. DREAM.3D v6.5.171 uses a different writer and has no such forwarding chain. + +**Affected users:** Users who generate XDMF sidecars for edge, surface or volume node geometries with attributes handled by a lower-dimensional helper. + +**Recommendation:** Trust SIMPLNX. Regenerate affected sidecars with DREAM3D-NX v7.4.2 or later. + +**Evidence:** PR #1683 (`9bcf794e9`) fixes all three calls in `src/simplnx/Utilities/Parsing/DREAM3D/Dream3dIO.cpp`. Direct source checks of every `upstream/release/v7.0.0` through `upstream/release/v7.4.1` show the exchanged arguments; `upstream/release/v7.4.2` has the corrections. `CheckXdmfFile()` in `src/Plugins/SimplnxCore/test/DREAM3DFileTest.cpp` checks filename prefixes in the pipeline/sidecar combination and file-I/O tests. It does not resolve every referenced HDF5 dataset. SIMPL `0afaa6854`, `Source/SIMPLib/DataContainers/DataContainer.cpp`, passes the container name and HDF5 filename separately to its geometry and attribute writers. + +--- + +### WriteDREAM3DFilter-D4 + +| Field | Value | +|---|---| +| **Deviation ID** | `WriteDREAM3DFilter-D4` | +| **Filter UUID** | `b3a95784-2ced-41ec-8d3d-0242ac130003` | +| **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 shared file reader could access an invalid result instead of returning a DataStructure import error. + +**Root cause:** Bug. After a successful pipeline import, the shared `ReadFile()` utility imported the DataStructure but checked `pipeline.invalid()` again. A failed DataStructure import then reached `dataStructure.value()`. The reader now tests `dataStructure.invalid()` and returns its errors. DREAM.3D v6.5.171 does not use this combined Result-based reader. -**Root cause:** Library (incomplete port, out of scope for this V&V cycle). `StatsDataArray` and `StructArray` are DREAM3D 6.5.171 `DataObject` types whose simplnx equivalents are being implemented on a separate, not-yet-merged branch. `WriteDREAM3DFilter`'s own write path (`Algorithms/WriteDREAM3D.cpp`) has no special-case logic to reject or special-case these types — the gap is entirely upstream, in `DataStructure`/`HDF5::DataStructureWriter` not yet having a type to construct. This V&V pass covers everything `WriteDREAM3DFilter` can currently write; it makes no claim about statistics data because that data cannot currently exist in a simplnx `DataStructure`. +**Affected users:** Callers of the shared `ReadFile` utility, including writer round-trip checks, when pipeline import succeeds but DataStructure import fails. -**Affected users:** Anyone migrating a legacy pipeline that computes per-ensemble statistics. Their exported SIMPLNX `.dream3d` file will simply lack the statistics group entirely (there being no such object to write) until the separate `StatsDataArray`/`StructArray` port lands and this filter is re-verified against it. +**Recommendation:** Trust SIMPLNX. Use DREAM3D-NX v7.4.2 or later to receive the DataStructure import error. -**Recommendation:** Trust 6.5.171 for statistics data until the pending port lands. This is a temporary feature gap, not a correctness defect in `WriteDREAM3DFilter` itself — re-run this V&V cycle's oracle tests once `StatsDataArray`/`StructArray` exist in this branch to confirm the writer handles them correctly. +**Evidence:** PR #1683 (`9bcf794e9`) changes the tested result in `src/simplnx/Utilities/Parsing/DREAM3D/Dream3dIO.cpp`. Every release source from `upstream/release/v7.0.0` through `upstream/release/v7.4.1` repeats the pipeline check; `upstream/release/v7.4.2` checks the DataStructure result. SIMPL `0afaa6854`, `Source/SIMPLib/CoreFilters/DataContainerReader.cpp`, uses a separate DataContainerArray reader. The writer tests cover successful combined reads but do not inject this DataStructure import failure. From 58b61b07ca4d48e33e2b1e5471f0c4381081ceb4 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Thu, 22 Oct 2026 09:43:12 -0400 Subject: [PATCH 63/91] DOC: Add the DREAM.3D v6.5.171 comparison to the RequireMinNumNeighbors V&V record Signed-off-by: Michael Jackson --- src/Plugins/SimplnxCore/vv/RequireMinNumNeighborsFilter.md | 4 ++-- .../SimplnxCore/vv/deviations/RequireMinNumNeighborsFilter.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Plugins/SimplnxCore/vv/RequireMinNumNeighborsFilter.md b/src/Plugins/SimplnxCore/vv/RequireMinNumNeighborsFilter.md index a49ef53fe9..adc71ebd03 100644 --- a/src/Plugins/SimplnxCore/vv/RequireMinNumNeighborsFilter.md +++ b/src/Plugins/SimplnxCore/vv/RequireMinNumNeighborsFilter.md @@ -19,9 +19,9 @@ | Code paths enumerated | 20 of 30 paths exercised. Gaps include cancellation, store failures and defensive helper paths. | | Tests today | 14 test cases cover both phase modes, exact cell copying, feature compaction, errors, shared paths, Small IN100 and SIMPL conversion. All pass in CI. | | Exemplar archive | `6_5_test_data_1_v2.tar.gz` supplies Small IN100 input. The analytical expectations are encoded in the tests. | -| Legacy comparison | Not run — comparison artifacts are not available. | +| Legacy comparison | Run against DREAM.3D v6.5.171 on 2026-07-28: all six arrays matched in both phase modes. The run predates the DREAM3D-NX algorithm changes of 2026-09-20; run the comparison again on the current algorithm. | | Bug flags | None | -| V&V phase | Analytical verification and second-engineer sign-off are complete. Legacy comparison artifacts are not available. | +| V&V phase | Analytical verification and second-engineer sign-off are complete. A legacy comparison with the current algorithm remains open. | ## Summary diff --git a/src/Plugins/SimplnxCore/vv/deviations/RequireMinNumNeighborsFilter.md b/src/Plugins/SimplnxCore/vv/deviations/RequireMinNumNeighborsFilter.md index 7f9e95c8e3..df611fe93c 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/RequireMinNumNeighborsFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/RequireMinNumNeighborsFilter.md @@ -70,4 +70,4 @@ Entries use the stable `RequireMinNumNeighborsFilter-D` sequence. The Filter **Recommendation:** `trust SIMPLNX`. The deterministic error prevents an unbounded execution and tells the user how to correct the input. -**Evidence:** `src/Plugins/SimplnxCore/test/RequireMinNumNeighborsTest.cpp::Execute Error - no coarsening progress (-55572)` checks this correction. Source at each release from `upstream/release/v7.0.0` through `upstream/release/v7.4.1` contains the defect; `upstream/release/v7.4.2` contains the fix from PR #1694 (`0c8fa9f59`). DREAM.3D `v6.5.171`, `Source/Plugins/Processing/ProcessingFilters/MinNeighbors.cpp`, contains the defective path. +**Evidence:** `src/Plugins/SimplnxCore/test/RequireMinNumNeighborsTest.cpp::Execute Error - no coarsening progress (-55572)` checks this correction. Source at each release from `upstream/release/v7.0.0` through `upstream/release/v7.4.1` contains the defect; `upstream/release/v7.4.2` contains the fix from PR #1694 (`0c8fa9f59`). DREAM.3D v6.5.171, `Source/Plugins/Processing/ProcessingFilters/MinNeighbors.cpp`, contains the defective path. `vv_work:RequireMinNumNeighborsFilter/ReadMe.md` records the 2026-07-28 run on `vv_work:RequireMinNumNeighborsFilter/hang_fixture.dream3d`, with `FeatureIds = [-1,-1,-1,-1]` and one retained feature. Both DREAM.3D v6.5.171 and the reviewed SIMPLNX version (`5f1c7a659`) failed to terminate and were stopped after 25 seconds. This confirms the shared defect before the fix; it does not describe current SIMPLNX behavior. From e0c77c47312da750c8d384ae96feb570244f1e48 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Thu, 22 Oct 2026 09:43:12 -0400 Subject: [PATCH 64/91] DOC: Add the DREAM.3D v6.5.171 comparison to the MultiThresholdObjects V&V record Signed-off-by: Michael Jackson --- src/Plugins/SimplnxCore/vv/MultiThresholdObjectsFilter.md | 4 ++-- .../vv/deviations/MultiThresholdObjectsFilter.md | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Plugins/SimplnxCore/vv/MultiThresholdObjectsFilter.md b/src/Plugins/SimplnxCore/vv/MultiThresholdObjectsFilter.md index 47e85af4d5..b496684c87 100644 --- a/src/Plugins/SimplnxCore/vv/MultiThresholdObjectsFilter.md +++ b/src/Plugins/SimplnxCore/vv/MultiThresholdObjectsFilter.md @@ -19,9 +19,9 @@ | Code paths enumerated | 36 of 51 exercised. Gaps include large chunks, several cancellation points, single-option custom values and basic pipeline conversion. | | Tests today | 27 test declarations: 23 ordinary cases and 4 template cases. They cover comparisons, nested logic, data types, validation, I/O errors, cancellation and conversion. | | Exemplar archive | None. Tests construct inputs and expected masks in memory. | -| Legacy comparison | Not run — comparison artifacts are not available. | +| Legacy comparison | Run against DREAM.3D v6.5.171 on 2026-07-23: all three cases matched. The run predates the DREAM3D-NX algorithm changes of 2026-09-20 and must run again on the current algorithm. | | Bug flags | None | -| V&V phase | Analytical verification and second-engineer review are complete. Three fixed defects and three capability differences are documented. | +| V&V phase | Analytical verification and second-engineer review are complete. Three fixed defects and three capability differences are documented. A legacy comparison with the current algorithm remains open. | ## Summary diff --git a/src/Plugins/SimplnxCore/vv/deviations/MultiThresholdObjectsFilter.md b/src/Plugins/SimplnxCore/vv/deviations/MultiThresholdObjectsFilter.md index c784a574d1..7b817abf15 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/MultiThresholdObjectsFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/MultiThresholdObjectsFilter.md @@ -22,7 +22,7 @@ Part 1 lists three capability differences from DREAM.3D v6.5.171. Part 2 lists t **Recommendation:** trust SIMPLNX. Select the required component explicitly; a legacy pipeline must first extract that component into a scalar array. -**Evidence:** `src/Plugins/SimplnxCore/test/MultiThresholdObjectsVvTest.cpp`, `Valid Single Thresholds: Int Multi-Component`, checks indices 0, 1 and 2. Legacy `Source/SIMPLib/CoreFilters/MultiThresholdObjects.cpp` and `MultiThresholdObjects2.cpp` contain the scalar-array checks in the SIMPL snapshot `0afaa6854`, selected as the last revision on or before the DREAM.3D `v6.5.171` tag date, 2026-05-27. +**Evidence:** `src/Plugins/SimplnxCore/test/MultiThresholdObjectsVvTest.cpp`, `Valid Single Thresholds: Int Multi-Component`, checks indices 0, 1 and 2. Legacy `Source/SIMPLib/CoreFilters/MultiThresholdObjects.cpp` and `MultiThresholdObjects2.cpp` contain the scalar-array checks in the SIMPL snapshot `0afaa6854`, selected as the last revision on or before the DREAM.3D v6.5.171 tag date, 2026-05-27. `vv_work:MultiThresholdObjectsFilter/ReadMe.md` confirms that `MultiThresholdObjects2::dataCheck()` rejects non-scalar arrays with error `-11003`. --- @@ -86,7 +86,7 @@ Part 1 lists three capability differences from DREAM.3D v6.5.171. Part 2 lists t **Recommendation:** trust SIMPLNX. Use DREAM3D-NX v7.4.2 or later and regenerate masks produced with affected nested trees. -**Evidence:** `git show` inspection of all ten release sources places the defect in v7.0.0 through v7.4.1 and the correction in v7.4.2. The affected code is in `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/MultiThresholdObjectsFilter.cpp` through v7.4.0 and `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/MultiThresholdObjects.cpp` in v7.4.1. PR #1688 (`467a250d1`) supplies the correction. `src/Plugins/SimplnxCore/test/MultiThresholdObjectsVvTest.cpp`, `Valid Threshold Sets` / `ArraySet 6: leaf + nested set`, checks the corrected mask. Legacy `Source/SIMPLib/CoreFilters/MultiThresholdObjects2.cpp`, `thresholdSet()`, in SIMPL snapshot `0afaa6854` uses its set buffer for child evaluation. +**Evidence:** `git show` inspection of all ten release sources places the defect in v7.0.0 through v7.4.1 and the correction in v7.4.2. The affected code is in `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/MultiThresholdObjectsFilter.cpp` through v7.4.0 and `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/MultiThresholdObjects.cpp` in v7.4.1. PR #1688 (`467a250d1`) supplies the correction. `src/Plugins/SimplnxCore/test/MultiThresholdObjectsVvTest.cpp`, `Valid Threshold Sets` / `ArraySet 6: leaf + nested set`, checks the corrected mask. Legacy `Source/SIMPLib/CoreFilters/MultiThresholdObjects2.cpp`, `thresholdSet()`, in SIMPL snapshot `0afaa6854` uses its set buffer for child evaluation. `vv_work:MultiThresholdObjectsFilter/ReadMe.md` records the 2026-07-23 comparison: corrected SIMPLNX masks matched DREAM.3D v6.5.171 and the independent NumPy oracle in all three 100-tuple cases. Before the correction, AB2 produced an all-false mask with 38 of 100 values wrong; AB1 matched. `vv_work:MultiThresholdObjectsFilter/compare.py` compares input values and checks each mask against the legacy output and the oracle after conversion to boolean. The run predates the DREAM3D-NX algorithm changes of 2026-09-20; a comparison with the current algorithm remains open. --- @@ -108,7 +108,7 @@ Part 1 lists three capability differences from DREAM.3D v6.5.171. Part 2 lists t **Recommendation:** trust SIMPLNX. Use DREAM3D-NX v7.4.2 or later and regenerate masks made with inverted threshold trees. -**Evidence:** All release sources through v7.4.1 contain `std::reverse()` in the threshold evaluator at the paths identified under D1. The v7.4.2 source applies logical inversion in `InsertThreshold()` and at the final root write. `src/Plugins/SimplnxCore/test/MultiThresholdObjectsVvTest.cpp`, `Valid Threshold Sets` / `ArraySet 7: leaf + inverted nested set`, checks nested inversion. `Top-level inversion is logical` and `Nested sets compose before root inversion` in `src/Plugins/SimplnxCore/test/MultiThresholdObjectsTest.cpp` check the root. Legacy `MultiThresholdObjects2.cpp` in SIMPL snapshot `0afaa6854` contains the corresponding logical inversion paths. +**Evidence:** All release sources through v7.4.1 contain `std::reverse()` in the threshold evaluator at the paths identified under D1. The v7.4.2 source applies logical inversion in `InsertThreshold()` and at the final root write. `src/Plugins/SimplnxCore/test/MultiThresholdObjectsVvTest.cpp`, `Valid Threshold Sets` / `ArraySet 7: leaf + inverted nested set`, checks nested inversion. `Top-level inversion is logical` and `Nested sets compose before root inversion` in `src/Plugins/SimplnxCore/test/MultiThresholdObjectsTest.cpp` check the root. Legacy `MultiThresholdObjects2.cpp` in SIMPL snapshot `0afaa6854` contains the corresponding logical inversion paths. `vv_work:MultiThresholdObjectsFilter/ReadMe.md` records 51 of 100 incorrect values in AB3 before the correction. This case combines an inverted nested set with OR. On 2026-07-23, the corrected mask matched DREAM.3D v6.5.171 and the independent NumPy oracle. The run predates the DREAM3D-NX algorithm changes of 2026-09-20 and does not verify the current algorithm. --- From 24f30078e6803fc44e5c613b4660ba6dd8c574c9 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Thu, 22 Oct 2026 09:43:54 -0400 Subject: [PATCH 65/91] DOC: Cite the primary 2026-07-31 legacy comparison in the RequireMinNumNeighbors V&V record Signed-off-by: Michael Jackson --- src/Plugins/SimplnxCore/vv/RequireMinNumNeighborsFilter.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Plugins/SimplnxCore/vv/RequireMinNumNeighborsFilter.md b/src/Plugins/SimplnxCore/vv/RequireMinNumNeighborsFilter.md index adc71ebd03..8c8d6cbc08 100644 --- a/src/Plugins/SimplnxCore/vv/RequireMinNumNeighborsFilter.md +++ b/src/Plugins/SimplnxCore/vv/RequireMinNumNeighborsFilter.md @@ -19,7 +19,7 @@ | Code paths enumerated | 20 of 30 paths exercised. Gaps include cancellation, store failures and defensive helper paths. | | Tests today | 14 test cases cover both phase modes, exact cell copying, feature compaction, errors, shared paths, Small IN100 and SIMPL conversion. All pass in CI. | | Exemplar archive | `6_5_test_data_1_v2.tar.gz` supplies Small IN100 input. The analytical expectations are encoded in the tests. | -| Legacy comparison | Run against DREAM.3D v6.5.171 on 2026-07-28: all six arrays matched in both phase modes. The run predates the DREAM3D-NX algorithm changes of 2026-09-20; run the comparison again on the current algorithm. | +| Legacy comparison | Run against DREAM.3D v6.5.171 on 2026-07-31: 2,646 values in 22 arrays over four fixture and mode variants matched DREAM.3D and the analytical oracle. The run predates the DREAM3D-NX algorithm changes of 2026-09-20; run the comparison again on the current algorithm. | | Bug flags | None | | V&V phase | Analytical verification and second-engineer sign-off are complete. A legacy comparison with the current algorithm remains open. | From df55c769c9d52fa1f5e2460be6563272afcdf4ae Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Thu, 22 Oct 2026 09:45:41 -0400 Subject: [PATCH 66/91] DOC: State why the ReadEbsdPatternFile V&V record has no legacy comparison Signed-off-by: Michael Jackson --- src/Plugins/OrientationAnalysis/vv/ReadEbsdPatternFileFilter.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Plugins/OrientationAnalysis/vv/ReadEbsdPatternFileFilter.md b/src/Plugins/OrientationAnalysis/vv/ReadEbsdPatternFileFilter.md index 7c6e7468ff..e0243d908c 100644 --- a/src/Plugins/OrientationAnalysis/vv/ReadEbsdPatternFileFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/ReadEbsdPatternFileFilter.md @@ -19,7 +19,7 @@ | 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 — comparison artifacts are not available. DREAM.3D v6.5.171 has no equivalent filter. | +| 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. | From 4410250d37ce3813b87ef0d66e287a9e53bc247c Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Thu, 22 Oct 2026 09:47:27 -0400 Subject: [PATCH 67/91] DOC: Mark the ReadEbsdPatternFile V&V record COMPLETE Signed-off-by: Michael Jackson --- src/Plugins/OrientationAnalysis/vv/ReadEbsdPatternFileFilter.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Plugins/OrientationAnalysis/vv/ReadEbsdPatternFileFilter.md b/src/Plugins/OrientationAnalysis/vv/ReadEbsdPatternFileFilter.md index e0243d908c..4df21ac484 100644 --- a/src/Plugins/OrientationAnalysis/vv/ReadEbsdPatternFileFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/ReadEbsdPatternFileFilter.md @@ -7,7 +7,7 @@ | SIMPLNX Human Name | Read EBSD Pattern File | | DREAM.3D v6.5.171 equivalent | None (new filter) | | Verified commit | ** | -| Status | DRAFT | +| Status | COMPLETE | | Sign-off | Michael Jackson, 2026-08-27; second engineer: Jared Duffey, 2026-09-15 | ## At a glance From a3d2472bbe00501ec093998d0c3bd22a2399b468 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Thu, 22 Oct 2026 09:49:28 -0400 Subject: [PATCH 68/91] DOC: Cite only the primary V&V package in the RequireMinNumNeighbors deviations Signed-off-by: Michael Jackson --- .../SimplnxCore/vv/deviations/RequireMinNumNeighborsFilter.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Plugins/SimplnxCore/vv/deviations/RequireMinNumNeighborsFilter.md b/src/Plugins/SimplnxCore/vv/deviations/RequireMinNumNeighborsFilter.md index df611fe93c..122b10233e 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/RequireMinNumNeighborsFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/RequireMinNumNeighborsFilter.md @@ -70,4 +70,4 @@ Entries use the stable `RequireMinNumNeighborsFilter-D` sequence. The Filter **Recommendation:** `trust SIMPLNX`. The deterministic error prevents an unbounded execution and tells the user how to correct the input. -**Evidence:** `src/Plugins/SimplnxCore/test/RequireMinNumNeighborsTest.cpp::Execute Error - no coarsening progress (-55572)` checks this correction. Source at each release from `upstream/release/v7.0.0` through `upstream/release/v7.4.1` contains the defect; `upstream/release/v7.4.2` contains the fix from PR #1694 (`0c8fa9f59`). DREAM.3D v6.5.171, `Source/Plugins/Processing/ProcessingFilters/MinNeighbors.cpp`, contains the defective path. `vv_work:RequireMinNumNeighborsFilter/ReadMe.md` records the 2026-07-28 run on `vv_work:RequireMinNumNeighborsFilter/hang_fixture.dream3d`, with `FeatureIds = [-1,-1,-1,-1]` and one retained feature. Both DREAM.3D v6.5.171 and the reviewed SIMPLNX version (`5f1c7a659`) failed to terminate and were stopped after 25 seconds. This confirms the shared defect before the fix; it does not describe current SIMPLNX behavior. +**Evidence:** `src/Plugins/SimplnxCore/test/RequireMinNumNeighborsTest.cpp::Execute Error - no coarsening progress (-55572)` checks this correction. Source at each release from `upstream/release/v7.0.0` through `upstream/release/v7.4.1` contains the defect; `upstream/release/v7.4.2` contains the fix from PR #1694 (`0c8fa9f59`). DREAM.3D v6.5.171, `Source/Plugins/Processing/ProcessingFilters/MinNeighbors.cpp`, contains the defective path. From d50e33908c7de4f75a7937ce3d6cce3515925264 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Thu, 22 Oct 2026 09:59:20 -0400 Subject: [PATCH 69/91] DOC: Record who generated the ComputeGroupingDensity v2 archive and when Signed-off-by: Michael Jackson --- .../SimplnxCore/vv/provenance/ComputeGroupingDensityFilter.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Plugins/SimplnxCore/vv/provenance/ComputeGroupingDensityFilter.md b/src/Plugins/SimplnxCore/vv/provenance/ComputeGroupingDensityFilter.md index 4f77bb66f4..c331a2f73f 100644 --- a/src/Plugins/SimplnxCore/vv/provenance/ComputeGroupingDensityFilter.md +++ b/src/Plugins/SimplnxCore/vv/provenance/ComputeGroupingDensityFilter.md @@ -9,8 +9,8 @@ This sidecar records the archive inputs, analytical expectations and local proof | **Archive** | `compute_grouping_densities_v2.tar.gz` | | **SHA512** | `3aaabb63c4fa16f7fa192ae4ee9dbba9394ec7f1cd19aff55e399a624d495a3a778c7f6f282911f681e85cea99e4c6d15344274e9107f337af7d4a19f93784ff` | | **Used by tests** | `SimplnxCore::ComputeGroupingDensityFilter: Exemplar A/B — all 4 configurations` | -| **Generated by** | Michael Jackson (inferred from PR #1631) | -| **Generated on** | — | +| **Generated by** | Michael Jackson (signed off in the archive `README.md`) | +| **Generated on** | 2026-05-27 | | **Generated at commit** | `ec22ac375` (PR #1631 merge, inferred) | `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. From 5b1f70f9d23b80dfdadeb2f97b9d793e555c20da Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Thu, 22 Oct 2026 13:22:02 -0400 Subject: [PATCH 70/91] DOC: State the legacy comparison evidence in the ComputeGroupingDensity V&V record Signed-off-by: Michael Jackson --- src/Plugins/SimplnxCore/vv/ComputeGroupingDensityFilter.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Plugins/SimplnxCore/vv/ComputeGroupingDensityFilter.md b/src/Plugins/SimplnxCore/vv/ComputeGroupingDensityFilter.md index 58bfe2ffae..e134274c2c 100644 --- a/src/Plugins/SimplnxCore/vv/ComputeGroupingDensityFilter.md +++ b/src/Plugins/SimplnxCore/vv/ComputeGroupingDensityFilter.md @@ -19,7 +19,7 @@ | 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 against DREAM.3D v6.5.171: it has no equivalent filter, and the comparison working folder is absent. The archive records a local proof comparison signed off on 2026-05-27; the algorithm source changed afterward. | +| 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. | From 86bf2c9e556c4a697ada881ae51843cb13f13d4a Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Thu, 22 Oct 2026 13:22:08 -0400 Subject: [PATCH 71/91] DOC: Add the 2026-10-22 legacy comparison to the ComputeFeatureNeighborCAxisMisalignments V&V record Signed-off-by: Michael Jackson --- .../vv/ComputeFeatureNeighborCAxisMisalignmentsFilter.md | 2 +- .../ComputeFeatureNeighborCAxisMisalignmentsFilter.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Plugins/OrientationAnalysis/vv/ComputeFeatureNeighborCAxisMisalignmentsFilter.md b/src/Plugins/OrientationAnalysis/vv/ComputeFeatureNeighborCAxisMisalignmentsFilter.md index e8ecda6234..dbf69fa1f5 100644 --- a/src/Plugins/OrientationAnalysis/vv/ComputeFeatureNeighborCAxisMisalignmentsFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/ComputeFeatureNeighborCAxisMisalignmentsFilter.md @@ -19,7 +19,7 @@ | 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 | Not run — comparison artifacts are not available. | +| 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. | diff --git a/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFeatureNeighborCAxisMisalignmentsFilter.md b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFeatureNeighborCAxisMisalignmentsFilter.md index 90a5750c5d..593080105b 100644 --- a/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFeatureNeighborCAxisMisalignmentsFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFeatureNeighborCAxisMisalignmentsFilter.md @@ -24,7 +24,7 @@ Entries use one stable ID sequence. D3 was not assigned to an entry and remains **Recommendation:** Trust SIMPLNX in DREAM3D-NX v7.4.2 or later. The average uses all accepted neighbors, regardless of their order. -**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. +**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`). --- @@ -44,7 +44,7 @@ Entries use one stable ID sequence. D3 was not assigned to an entry and remains **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. -**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. +**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`). --- @@ -64,7 +64,7 @@ Entries use one stable ID sequence. D3 was not assigned to an entry and remains **Recommendation:** Trust SIMPLNX. The same geometric c-axis angle applies to both hexagonal classes. -**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 fixture directly selects Hexagonal_Low. +**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`). --- From 480ee2fd79407c9075c1d01cbf3f0050b3f60287 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Thu, 22 Oct 2026 13:22:08 -0400 Subject: [PATCH 72/91] DOC: Add the 2026-10-22 legacy comparison to the ComputeFeatureNeighbors V&V record Signed-off-by: Michael Jackson --- src/Plugins/SimplnxCore/vv/ComputeFeatureNeighborsFilter.md | 2 +- .../vv/deviations/ComputeFeatureNeighborsFilter.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Plugins/SimplnxCore/vv/ComputeFeatureNeighborsFilter.md b/src/Plugins/SimplnxCore/vv/ComputeFeatureNeighborsFilter.md index 6093f0becd..06ed25b2d0 100644 --- a/src/Plugins/SimplnxCore/vv/ComputeFeatureNeighborsFilter.md +++ b/src/Plugins/SimplnxCore/vv/ComputeFeatureNeighborsFilter.md @@ -19,7 +19,7 @@ | Code paths enumerated | 24 of 34 exercised by the mapped tests. Error returns, cancellation, negative IDs and the unreachable dispatch fallback have no direct fixture. | | Tests today | 38 test cases: 35 analytical cases, one archive regression, one SIMPL conversion 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 | Not run — comparison artifacts are not available. | +| 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. | diff --git a/src/Plugins/SimplnxCore/vv/deviations/ComputeFeatureNeighborsFilter.md b/src/Plugins/SimplnxCore/vv/deviations/ComputeFeatureNeighborsFilter.md index f3dbe6fc7a..4235e1b756 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/ComputeFeatureNeighborsFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/ComputeFeatureNeighborsFilter.md @@ -24,7 +24,7 @@ The deviation IDs are stable. The Filter UUID is the permanent cross-reference a **Recommendation:** Trust SIMPLNX. DREAM3D-NX v7.4.2 and later use the geometric area of each shared face. The analytical 2D and 3D fixtures check direction-specific areas independently of the archive. -**Evidence:** The DREAM.3D `v6.5.171` source at `Source/Plugins/Statistics/StatisticsFilters/FindNeighbors.cpp` contains `float area = float(number) * xRes * yRes`. Each `upstream/release/v7.0.0` through `upstream/release/v7.4.1` source contains the same formula. The v7.4.2 source contains per-face accumulation. PR #1569 (`9e61b41d4`) introduced the correction; the release-source table is in `../provenance/ComputeFeatureNeighborsFilter.md`. +**Evidence:** The DREAM.3D v6.5.171 source at `Source/Plugins/Statistics/StatisticsFilters/FindNeighbors.cpp` contains `float area = float(number) * xRes * yRes`. Each DREAM3D-NX release from v7.0.0 through v7.4.1 contains the same source formula. The v7.4.2 source contains per-face accumulation. PR #1569 (`9e61b41d4`) introduced the correction; the release-source table is in `../provenance/ComputeFeatureNeighborsFilter.md`. The 2026-10-22 comparison in `vv_work:ComputeFeatureNeighborsFilter/comparison/LegacyComparison.md` records 16 SharedSurfaceAreaList value differences between DREAM.3D v6.5.171 and DREAM3D-NX in `anisotropic_xyz`, with an exact match for a local proof version that applies the fixes documented in `vv_work:ComputeFeatureNeighborsFilter/notes/proof_commits.md`. --- @@ -46,4 +46,4 @@ The deviation IDs are stable. The Filter UUID is the permanent cross-reference a **Recommendation:** Trust SIMPLNX. DREAM3D-NX v7.4.2 and later mark only features that touch the boundary in an active dimension. The 1D and 2D analytical fixtures assert false flags for interior features. -**Evidence:** The tagged `FindNeighbors.cpp` source tests `column == 0` and `row == 0` in both boundary conditions. Each `upstream/release/v7.0.0` through `upstream/release/v7.4.1` source retains those conditions. The v7.4.2 source uses dimensionality-specific marking. Current tests `Case 1.0.0`, `Case 1.1.0`, `Case 1.2.0`, `Case 2.1.0` and `Case 2.2.0` assert the corrected flags. +**Evidence:** The tagged `FindNeighbors.cpp` source tests `column == 0` and `row == 0` in both boundary conditions. Each DREAM3D-NX release from v7.0.0 through v7.4.1 retains those source conditions. The v7.4.2 source uses dimensionality-specific marking. Current tests `Case 1.0.0`, `Case 1.1.0`, `Case 1.2.0`, `Case 2.1.0` and `Case 2.2.0` assert the corrected flags. The 2026-10-22 comparison in `vv_work:ComputeFeatureNeighborsFilter/comparison/LegacyComparison.md` records two SurfaceFeatures flags that change from 1 in DREAM.3D v6.5.171 to 0 in DREAM3D-NX in each of `unit_y_surface` and `line_z_surface`, with exact matches for a local proof version that applies the fixes documented in `vv_work:ComputeFeatureNeighborsFilter/notes/proof_commits.md`. From 60b5e9e5e4640e52db962548bf3703c03ba729f3 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Thu, 22 Oct 2026 13:22:08 -0400 Subject: [PATCH 73/91] DOC: Add the 2026-10-22 legacy comparison to the ComputeFeatureCentroids V&V record Signed-off-by: Michael Jackson --- src/Plugins/SimplnxCore/vv/ComputeFeatureCentroidsFilter.md | 2 +- .../vv/deviations/ComputeFeatureCentroidsFilter.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Plugins/SimplnxCore/vv/ComputeFeatureCentroidsFilter.md b/src/Plugins/SimplnxCore/vv/ComputeFeatureCentroidsFilter.md index 8316deef5a..df205a3975 100644 --- a/src/Plugins/SimplnxCore/vv/ComputeFeatureCentroidsFilter.md +++ b/src/Plugins/SimplnxCore/vv/ComputeFeatureCentroidsFilter.md @@ -19,7 +19,7 @@ | Code paths enumerated | 16 of 26 exercised by the five ordinary test cases. The table identifies validation, cancellation, I/O and periodic-axis gaps. | | Tests today | 6 test cases: analytical centroids, periodic centroids, invalid IDs, bulk reads, SIMPL conversion and a hidden storage-contract fixture. | | Exemplar archive | None. The tests construct inputs and expected values in C++. | -| Legacy comparison | Not run — comparison artifacts are not available. | +| 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. | diff --git a/src/Plugins/SimplnxCore/vv/deviations/ComputeFeatureCentroidsFilter.md b/src/Plugins/SimplnxCore/vv/deviations/ComputeFeatureCentroidsFilter.md index dd7075c27b..be126ed080 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/ComputeFeatureCentroidsFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/ComputeFeatureCentroidsFilter.md @@ -22,7 +22,7 @@ Part 1 records the coordinate-precision difference and the added periodic mode. **Recommendation:** Trust SIMPLNX. Its coordinate calculation avoids the earlier float32 rounding; compare numerical results with a tolerance appropriate to the data. -**Evidence:** `Source/Plugins/Generic/GenericFilters/FindFeatureCentroids.cpp` at the DREAM.3D `v6.5.171` tag contains the float coordinate buffer and Kahan kernel. The current SIMPLNX algorithm contains explicit double-precision coordinate expressions. Both sources divide each accumulated coordinate by its cell count. +**Evidence:** `Source/Plugins/Generic/GenericFilters/FindFeatureCentroids.cpp` in DREAM.3D v6.5.171 contains the float coordinate buffer and Kahan kernel. The current SIMPLNX algorithm contains explicit double-precision coordinate expressions. Both sources divide each accumulated coordinate by its cell count. The 2026-10-22 comparison records an exact match across all three versions for `neutral_B` and feature 1 X = 16777216 in DREAM.3D v6.5.171 versus 16777218 in DREAM3D-NX and a local proof version with the D1 fix for `precision_D1` (`vv_work:ComputeFeatureCentroidsFilter/comparison/LegacyComparison.md`, `vv_work:ComputeFeatureCentroidsFilter/notes/proof_commits.md`). --- @@ -42,7 +42,7 @@ Part 1 records the coordinate-precision difference and the added periodic mode. **Recommendation:** Trust SIMPLNX. Enable periodic mode for periodic geometry; keep it disabled for ordinary arithmetic centroids. D3 and D4 describe corrections to this added mode. -**Evidence:** `ComputeFeatureCentroidsFilter::parameters()` defines the false default, and `FromSIMPLJson()` starts from default arguments. DREAM.3D `v6.5.171` `FindFeatureCentroids` has no periodic parameter or calculation. Fixtures D–F in `src/Plugins/SimplnxCore/test/ComputeFeatureCentroidsTest.cpp` check the periodic behavior independently. +**Evidence:** `ComputeFeatureCentroidsFilter::parameters()` defines the false default, and `FromSIMPLJson()` starts from default arguments. DREAM.3D v6.5.171 `FindFeatureCentroids` has no periodic parameter or calculation. Fixtures D–F in `src/Plugins/SimplnxCore/test/ComputeFeatureCentroidsTest.cpp` check the periodic behavior independently. The 2026-10-22 case `periodic_E_D2` gives feature 1 X = 13.666666984558105 in both DREAM.3D v6.5.171 and a local proof version versus 11 in DREAM3D-NX because only DREAM3D-NX has the periodic option; no D2 proof patch is needed (`vv_work:ComputeFeatureCentroidsFilter/comparison/LegacyComparison.md`, `vv_work:ComputeFeatureCentroidsFilter/notes/proof_commits.md`). --- @@ -88,4 +88,4 @@ Part 1 records the coordinate-precision difference and the added periodic mode. **Recommendation:** Trust SIMPLNX. Use the DREAM3D-NX release after v7.4.2 for the corrected periodic calculation. Disable periodic mode only when an ordinary arithmetic centroid is appropriate. -**Evidence:** The periodic implementation in `upstream/release/v7.2.1` through `upstream/release/v7.4.2` calls the constant-offset image helper. PR #1670 (`4a7b71e5f`) replaces that model with the circular mean and removes the image helper. Current fixtures D and E independently unwrap coordinates to obtain X = 3.5 and X = 11; fixture F requires the arithmetic fallback X = 2. +**Evidence:** The periodic implementation in DREAM3D-NX v7.2.1 through v7.4.2 calls the constant-offset image helper. PR #1670 (`4a7b71e5f`) replaces that model with the circular mean and removes the image helper. Current fixtures D and E independently unwrap coordinates to obtain X = 3.5 and X = 11; fixture F requires the arithmetic fallback X = 2. From 32469e3e9dc9890d4f801fca49fcf411632e9622 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Thu, 22 Oct 2026 13:22:08 -0400 Subject: [PATCH 74/91] DOC: Add the 2026-10-22 legacy comparison to the ComputeNeighborhoods V&V record Signed-off-by: Michael Jackson --- src/Plugins/SimplnxCore/vv/ComputeNeighborhoodsFilter.md | 4 ++-- .../vv/deviations/ComputeNeighborhoodsFilter.md | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Plugins/SimplnxCore/vv/ComputeNeighborhoodsFilter.md b/src/Plugins/SimplnxCore/vv/ComputeNeighborhoodsFilter.md index dd72712f10..3a2119fcf3 100644 --- a/src/Plugins/SimplnxCore/vv/ComputeNeighborhoodsFilter.md +++ b/src/Plugins/SimplnxCore/vv/ComputeNeighborhoodsFilter.md @@ -19,9 +19,9 @@ | 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 | Not run — comparison artifacts are not available. | +| 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 are complete. The sign-off has no named second engineer. | +| V&V phase | Verification records, second-engineer review and the legacy comparison are complete. | ## Summary diff --git a/src/Plugins/SimplnxCore/vv/deviations/ComputeNeighborhoodsFilter.md b/src/Plugins/SimplnxCore/vv/deviations/ComputeNeighborhoodsFilter.md index 9127831182..cf0ac9495d 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/ComputeNeighborhoodsFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/ComputeNeighborhoodsFilter.md @@ -16,13 +16,13 @@ Part 1 lists differences from `FindNeighborhoods` in DREAM.3D v6.5.171 (D1–D3) **Symptom:** The same centroids, diameters, and multiplier can give different neighbor counts and lists in SIMPLNX and DREAM.3D v6.5.171. -**Root cause:** Algorithmic choice. DREAM.3D v6.5.171 converts centroids to integer bins using the average diameter. It requires each absolute bin difference to be strictly less than `equivalentDiameter[i] * multiplier / averageDiameter`. SIMPLNX compares squared Euclidean distance with `(equivalentDiameter[i] * multiplier)^2` and includes equality. The average diameter controls the SIMPLNX search grid, not the distance limit. SIMPLNX also offers an absolute-radius mode without Equivalent Diameters; DREAM.3D v6.5.171 has no such mode. +**Root cause:** Algorithmic choice. DREAM.3D v6.5.171 converts centroids to integer bins using the average diameter. It requires each absolute bin difference to be strictly less than `equivalentDiameter[i] * multiplier / averageDiameter`. SIMPLNX compares squared Euclidean distance with `(equivalentDiameter[i] * multiplier)^2` and includes equality. The average diameter controls the SIMPLNX search grid, not the distance limit. SIMPLNX also offers an absolute-radius mode without Equivalent Diameters; DREAM.3D v6.5.171 has no such mode. SIMPLNX visits bins in X, Y, Z order and features within each bin in ID order, so list order differs from the DREAM.3D v6.5.171 candidate scan even when membership is equal. **Affected users:** Users who migrate `FindNeighborhoods` pipelines and compare neighbor counts or lists. Bin quantization and the different inclusion rules prevent a fixed count ratio between implementations. **Recommendation:** Trust SIMPLNX for centroid-distance neighborhoods. The sphere implements the stated search radius, and both radius modes have independent analytical fixtures. The UUID mapping preserves pipeline conversion but does not promise identical output from the different neighborhood definitions. -**Evidence:** `Source/Plugins/Statistics/StatisticsFilters/FindNeighborhoods.cpp` at the DREAM.3D `v6.5.171` tag contains the strict bin comparisons. Current `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeNeighborhoods.cpp` contains the inclusive distance comparison. `SimplnxCore::ComputeNeighborhoods_SyntheticOracle` checks the exact-radius boundary, and `SimplnxCore::ComputeNeighborhoods_MultiplesAnalyticalOracle` checks per-feature asymmetry in `src/Plugins/SimplnxCore/test/ComputeNeighborhoodsTest.cpp`. +**Evidence:** `Source/Plugins/Statistics/StatisticsFilters/FindNeighborhoods.cpp` at the DREAM.3D `v6.5.171` tag contains the strict bin comparisons. Current `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeNeighborhoods.cpp` contains the inclusive distance comparison. `SimplnxCore::ComputeNeighborhoods_SyntheticOracle` checks the exact-radius boundary, and `SimplnxCore::ComputeNeighborhoods_MultiplesAnalyticalOracle` checks per-feature asymmetry in `src/Plugins/SimplnxCore/test/ComputeNeighborhoodsTest.cpp`. On 2026-10-22, `neutral_absolute_oracle` matches all three implementations, `multiples_analytical_oracle` differs from stock in one list order, and `d1_boundary_box_quantization` differs from stock in six counts and six lists; a local proof version matches DREAM3D-NX exactly in all three cases (`vv_work:ComputeNeighborhoodsFilter/comparison/LegacyComparison.md`, `vv_work:ComputeNeighborhoodsFilter/notes/proof_commits.md`). --- @@ -42,7 +42,7 @@ Part 1 lists differences from `FindNeighborhoods` in DREAM.3D v6.5.171 (D1–D3) **Recommendation:** Trust SIMPLNX. Select Centroids and, for multiples mode, Equivalent Diameters; no phase array is required. -**Evidence:** The tagged legacy `FindNeighborhoods.cpp` requires Feature Phases only during input validation. Current `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeNeighborhoodsFilter.cpp` has no phase parameter or `FromSIMPLJson()` mapping. The two analytical fixtures create no phase array, and the SIMPL conversion test checks the remaining argument values. +**Evidence:** The tagged legacy `FindNeighborhoods.cpp` requires Feature Phases only during input validation. Current `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeNeighborhoodsFilter.cpp` has no phase parameter or `FromSIMPLJson()` mapping. The two analytical fixtures create no phase array, and the SIMPL conversion test checks the remaining argument values. On 2026-10-22, `neutral_absolute_oracle` includes neighbors with different phases and matches all three implementations without a D2 proof patch; the package supplies phases for legacy compatibility and has no dedicated D2 case (`vv_work:ComputeNeighborhoodsFilter/ReadMe.md`, `vv_work:ComputeNeighborhoodsFilter/notes/source_evidence.md`, `vv_work:ComputeNeighborhoodsFilter/comparison/LegacyComparison.md`). --- @@ -62,7 +62,7 @@ Part 1 lists differences from `FindNeighborhoods` in DREAM.3D v6.5.171 (D1–D3) **Recommendation:** Trust SIMPLNX. Supply a positive value for the selected radius mode. -**Evidence:** Current `ComputeNeighborhoodsFilter::preflightImpl()` contains both positivity checks. `SimplnxCore::ComputeNeighborhoods_InvalidSearchRadius` asserts both error codes with zero values. DREAM.3D v6.5.171 `FindNeighborhoods::dataCheck()` contains no equivalent check and has no absolute-radius mode. +**Evidence:** Current `ComputeNeighborhoodsFilter::preflightImpl()` contains both positivity checks. `SimplnxCore::ComputeNeighborhoods_InvalidSearchRadius` asserts both error codes with zero values. DREAM.3D v6.5.171 `FindNeighborhoods::dataCheck()` contains no equivalent check and has no absolute-radius mode. On 2026-10-22, the local proof version matches DREAM3D-NX in `neutral_absolute_oracle`, `multiples_analytical_oracle`, and `d1_boundary_box_quantization` with positive radius parameters; these cases do not check invalid inputs, and the package has no D3 case or D3 proof patch (`vv_work:ComputeNeighborhoodsFilter/ReadMe.md`, `vv_work:ComputeNeighborhoodsFilter/comparison/LegacyComparison.md`, `vv_work:ComputeNeighborhoodsFilter/notes/proof_commits.md`). --- From 02bcb10b1a3fdc94efa3f304563a38320f10b892 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Thu, 22 Oct 2026 13:22:08 -0400 Subject: [PATCH 75/91] DOC: Add the 2026-10-22 legacy comparison to the RegularizeZSpacing V&V record Signed-off-by: Michael Jackson --- src/Plugins/SimplnxCore/vv/RegularizeZSpacingFilter.md | 4 ++-- .../SimplnxCore/vv/deviations/RegularizeZSpacingFilter.md | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Plugins/SimplnxCore/vv/RegularizeZSpacingFilter.md b/src/Plugins/SimplnxCore/vv/RegularizeZSpacingFilter.md index 961a11d652..92ba85b42f 100644 --- a/src/Plugins/SimplnxCore/vv/RegularizeZSpacingFilter.md +++ b/src/Plugins/SimplnxCore/vv/RegularizeZSpacingFilter.md @@ -19,9 +19,9 @@ | Code paths enumerated | 13 of 22 exercised by the ordinary tests. Gaps include defensive guards, cancellation, grouped child copying and additional array types. | | Tests today | 6 test cases: 3 valid executions, 1 input-validation case, 1 SIMPL conversion case and 1 hidden HDF5 case. The five ordinary cases cover analytical values, geometry properties and error codes. | | Exemplar archive | None. The test source constructs inputs and defines analytical expected values. | -| Legacy comparison | Not run — comparison artifacts are not available. | +| Legacy comparison | Run against DREAM.3D v6.5.171 on 2026-10-22: 1 of 3 cases match; the differences come from RegularizeZSpacingFilter-D1 and RegularizeZSpacingFilter-D3. A local proof version also matches DREAM3D-NX in 1 of 3 cases; these are configuration and DREAM3D-NX-only option differences, not calculation differences. | | Bug flags | None | -| V&V phase | Analytical checks, input-validation checks and second-engineer review are complete. | +| V&V phase | Analytical checks, input-validation checks, legacy comparison and second-engineer review are complete. | ## Summary diff --git a/src/Plugins/SimplnxCore/vv/deviations/RegularizeZSpacingFilter.md b/src/Plugins/SimplnxCore/vv/deviations/RegularizeZSpacingFilter.md index 9bc18738af..54d963ef72 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/RegularizeZSpacingFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/RegularizeZSpacingFilter.md @@ -22,7 +22,7 @@ Part 1 records differences confirmed by source comparison with `RegularizeZSpaci **Recommendation:** Trust SIMPLNX after checking the converted selection. Assign the intended cell AttributeMatrix to the selected Image Geometry before execution. -**Evidence:** `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/RegularizeZSpacingFilter.cpp` uses `DataArraySelectionToGeometrySelectionFilterParameterConverter`, whose implementation in `src/simplnx/Parameters/GeometrySelectionParameter.cpp` retains only the DataContainer name. DREAM.3D v6.5.171 `Source/Plugins/Sampling/SamplingFilters/RegularizeZSpacing.cpp` validates and uses the full AttributeMatrix path. `SIMPL Backwards Compatibility` in `src/Plugins/SimplnxCore/test/RegularizeZSpacingTest.cpp` checks the converted geometry path. +**Evidence:** `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/RegularizeZSpacingFilter.cpp` uses `DataArraySelectionToGeometrySelectionFilterParameterConverter`, whose implementation in `src/simplnx/Parameters/GeometrySelectionParameter.cpp` retains only the DataContainer name. DREAM.3D v6.5.171 `Source/Plugins/Sampling/SamplingFilters/RegularizeZSpacing.cpp` validates and uses the full AttributeMatrix path. `SIMPL Backwards Compatibility` in `src/Plugins/SimplnxCore/test/RegularizeZSpacingTest.cpp` checks the converted geometry path. On 2026-10-22, `D1_matrix_selection` produced two cell-array shape differences against DREAM3D-NX for both DREAM.3D v6.5.171 and a local proof version, with matching geometry properties and no proof patch needed for this selection difference (`vv_work:RegularizeZSpacingFilter/comparison/LegacyComparison.md`, `vv_work:RegularizeZSpacingFilter/notes/proof_commits.md`). --- @@ -42,7 +42,7 @@ Part 1 records differences confirmed by source comparison with `RegularizeZSpaci **Recommendation:** Trust SIMPLNX. Supply at least `ZPoints + 1` parseable boundary values in nondecreasing order, with a positive final boundary. -**Evidence:** Compare `ReadZBoundsFile()` in `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/RegularizeZSpacing.cpp` and `preflightImpl()` in `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/RegularizeZSpacingFilter.cpp` with `dataCheck()` and `execute()` in DREAM.3D v6.5.171 `Source/Plugins/Sampling/SamplingFilters/RegularizeZSpacing.cpp`. The `Invalid Parameters` test checks short files, decreasing boundaries and zero extent with their exact error codes. +**Evidence:** Compare `ReadZBoundsFile()` in `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/RegularizeZSpacing.cpp` and `preflightImpl()` in `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/RegularizeZSpacingFilter.cpp` with `dataCheck()` and `execute()` in DREAM.3D v6.5.171 `Source/Plugins/Sampling/SamplingFilters/RegularizeZSpacing.cpp`. The `Invalid Parameters` test checks short files, decreasing boundaries and zero extent with their exact error codes. The 2026-10-22 package excludes D2 invalid-input cases and records an exact match across all three implementations for valid input in `neutral_in_place`, with no proof patch needed (`vv_work:RegularizeZSpacingFilter/ReadMe.md`, `vv_work:RegularizeZSpacingFilter/comparison/LegacyComparison.md`, `vv_work:RegularizeZSpacingFilter/notes/proof_commits.md`). --- @@ -62,4 +62,4 @@ Part 1 records differences confirmed by source comparison with `RegularizeZSpaci **Recommendation:** Trust SIMPLNX. Select the created geometry in downstream filters, or keep `Perform In Place` enabled for the legacy output arrangement. -**Evidence:** `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/RegularizeZSpacingFilter.cpp` creates output geometry and applies deferred replacement actions only in in-place mode. `Valid Execution (New Geometry)` and `Valid Execution (In Place)` in `src/Plugins/SimplnxCore/test/RegularizeZSpacingTest.cpp` check the two output arrangements. DREAM.3D v6.5.171 `Source/Plugins/Sampling/SamplingFilters/RegularizeZSpacing.cpp::execute()` replaces the selected AttributeMatrix. +**Evidence:** `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/RegularizeZSpacingFilter.cpp` creates output geometry and applies deferred replacement actions only in in-place mode. `Valid Execution (New Geometry)` and `Valid Execution (In Place)` in `src/Plugins/SimplnxCore/test/RegularizeZSpacingTest.cpp` check the two output arrangements. DREAM.3D v6.5.171 `Source/Plugins/Sampling/SamplingFilters/RegularizeZSpacing.cpp::execute()` replaces the selected AttributeMatrix. On 2026-10-22, `D3_separate_output` gave identical resampled values and result geometry properties in all three implementations, with four records for the retained input present only in DREAM3D-NX and no proof patch needed for this output option (`vv_work:RegularizeZSpacingFilter/comparison/LegacyComparison.md`, `vv_work:RegularizeZSpacingFilter/notes/proof_commits.md`). From c52ef6454680fde28ec10705442e0004314c422f Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Sat, 24 Oct 2026 09:29:30 -0400 Subject: [PATCH 76/91] DOC: Use the -59571 tuple-count error and its renamed test in the RequireMinNumNeighbors V&V record Signed-off-by: Michael Jackson --- src/Plugins/SimplnxCore/vv/RequireMinNumNeighborsFilter.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Plugins/SimplnxCore/vv/RequireMinNumNeighborsFilter.md b/src/Plugins/SimplnxCore/vv/RequireMinNumNeighborsFilter.md index 8c8d6cbc08..b1009e5c71 100644 --- a/src/Plugins/SimplnxCore/vv/RequireMinNumNeighborsFilter.md +++ b/src/Plugins/SimplnxCore/vv/RequireMinNumNeighborsFilter.md @@ -65,7 +65,7 @@ Test names below omit the common `SimplnxCore::RequireMinNumNeighborsFilter: ` p |---|-------|------|-----------| | 1 | Preflight | Feature array tuple counts differ → `-252`. | `Preflight Error - tuple count mismatch (-252)` | | 2 | Preflight | FeatureIds is outside the geometry's Cell Attribute Matrix → `-55575`. | `Preflight Error - FeatureIds outside Cell Attribute Matrix (-55575)`; `Strict Feature Ids` | -| 3 | Preflight | FeatureIds tuple count differs from the geometry cell count → `-55571`. | `Preflight Error - FeatureIds tuple count mismatch (-55571)` | +| 3 | Preflight | FeatureIds tuple count differs from the geometry cell count → `-59571`. | `Selected cell tuple counts` | | 4 | Preflight | An ignored array is outside the Cell Attribute Matrix → `-55573`. | `Strict Feature Ids` — `Ignored array in a second Attribute Matrix` | | 5 | Preflight | Number of Neighbors has an invalid Feature Attribute Matrix parent → `-55574`. | `Strict Feature Ids` — external, plain-group and Cell Attribute Matrix parents | | 6 | Paths | Resolve shared Cell paths and compact the selected Feature Attribute Matrix. | `Shared Cell Paths and Selected Feature Matrix` — both alias variants | @@ -115,7 +115,7 @@ All 14 test cases are in `src/Plugins/SimplnxCore/test/RequireMinNumNeighborsTes | `SimplnxCore::RequireMinNumNeighborsFilter: SIMPL Backwards Compatibility` | kept | Loads the two conversion fixtures below and verifies one converted filter, UUID, comments, and seven converted arguments in each section. | | `SIMPL 6.5 (UUID)` | kept | `DYNAMIC_SECTION` using `test/simpl_conversion/6_5/RequireMinNumNeighborsFilter.json`; verifies UUID-based 6.5 pipeline conversion. | | `SIMPL 6.4 (Filter_Name)` | kept | `DYNAMIC_SECTION` using `test/simpl_conversion/6_4/RequireMinNumNeighborsFilter.json`; verifies name-based 6.4 pipeline conversion. | -| `SimplnxCore::RequireMinNumNeighborsFilter: Preflight Error - FeatureIds tuple count mismatch (-55571)` | new-for-V&V | Creates a 5-cell geometry with four `FeatureIds` tuples in its Cell Attribute Matrix, then asserts invalid preflight and exact code `-55571`. | +| `SimplnxCore::RequireMinNumNeighborsFilter: Selected cell tuple counts` | new-for-V&V | Creates a 4-cell geometry with 3, 4, or 5 `FeatureIds` tuples stored separately. Equal counts pass; mismatches assert invalid preflight, exact code `-59571`, selected paths, tuple count, and cell count. | | `SimplnxCore::RequireMinNumNeighborsFilter: Strict Feature Ids` | new-for-V&V | Checks valid Cell and Feature Attribute Matrices, shared Cell paths, and read-only phases in another matrix. Rejects misplaced Feature Ids (`-55575`), ignored arrays (`-55573`), and invalid Feature Attribute Matrix locations (`-55574`); checks each error code and affected paths. | | `SimplnxCore::RequireMinNumNeighborsFilter: Shared Cell Paths and Selected Feature Matrix` | new-for-V&V | Alternates geometry and shared-parent paths for Feature Ids and ignored arrays. Checks geometry-based modification paths, reassigned IDs and cell values, unchanged ignored values, selected Feature Attribute Matrix compaction, unchanged external marker values, and inherited tuple dimensions. | From cc2353fc371da3060464489205dac36153639914 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Fri, 23 Oct 2026 17:00:06 -0400 Subject: [PATCH 77/91] BUG: Bound Feature Ids and accumulate in double in Compute Average C-Axis Orientations - Check at execute time that every Feature Id indexes the Average C-Axes array (error -76405). Before, an out-of-range Feature Id wrote past the end of the feature cache. - Accumulate the c-axis sums in double and convert to float32 once. Before, the float32 sums lost precision for large features. - Add tests for out-of-range Feature Ids and an analytical large-feature case. Both fail on the old code. Signed-off-by: Michael Jackson --- .../docs/ComputeAvgCAxesFilter.md | 2 +- .../Filters/Algorithms/ComputeAvgCAxes.cpp | 47 ++++++---- .../test/ComputeAvgCAxesTest.cpp | 89 ++++++++++++++++++- 3 files changed, 119 insertions(+), 19 deletions(-) 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/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/test/ComputeAvgCAxesTest.cpp b/src/Plugins/OrientationAnalysis/test/ComputeAvgCAxesTest.cpp index f161da0a7a..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(); @@ -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. From 8a882a5b7642f20a39fb92bde8e727e5f5752b08 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Fri, 23 Oct 2026 17:00:06 -0400 Subject: [PATCH 78/91] BUG: Bound Feature Ids and fix statistics in Compute Feature Reference C-Axis Misalignments - Check at execute time that every Feature Id indexes the Average C-Axes array (error -9806). Before, an out-of-range or negative Feature Id read and wrote past the end of the feature arrays. - Apply the same cell inclusion rule (positive Feature Id, positive hexagonal phase) in the standard deviation pass as in the mean pass. Before, excluded cells added zeros and inflated the standard deviation. - Accumulate the feature means in double and convert to float32 once. - Add tests for each defect. Each test fails on the old code. Signed-off-by: Michael Jackson --- ...tureReferenceCAxisMisorientationsFilter.md | 4 +- ...teFeatureReferenceCAxisMisorientations.cpp | 42 +++++- ...atureReferenceCAxisMisorientationsTest.cpp | 131 ++++++++++++++++++ 3 files changed, 170 insertions(+), 7 deletions(-) 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/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/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); +} From 08c0a38ca353e28e29c0e09fd5fa71302903cc35 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Fri, 23 Oct 2026 17:00:06 -0400 Subject: [PATCH 79/91] BUG: Validate Face Labels and accumulate in double in Compute Feature Centroids from Triangle Geometry - Preflight rejects an empty Face Labels array (-12903) and a Face Labels array whose tuple count is not the number of faces (-12902). Before, these inputs read out of bounds or requested a huge resize. - Resize the feature matrix only for positive labels, with 64-bit comparison. - Sum vertex coordinates in double and convert to float32 once. - The exemplar comparison tolerance changes from 0.0001 to 0.05. The archive holds float32-accumulated centroids; a float64 reference computed from the archive triangles differs from them by at most 0.0415 (feature 518). The archive is unchanged. Signed-off-by: Michael Jackson --- .../ComputeTriangleGeomCentroids.cpp | 24 +-- .../ComputeTriangleGeomCentroidsFilter.cpp | 16 ++ .../test/ComputeTriangleGeomCentroidsTest.cpp | 142 +++++++++++++++++- 3 files changed, 168 insertions(+), 14 deletions(-) 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/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/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); +} From 1b1531287a2e994ad818fab90b82e8c58c8b266d Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Fri, 23 Oct 2026 17:00:06 -0400 Subject: [PATCH 80/91] BUG: Fill holes on 2D images in Isolate Largest Feature (Identify Sample) The connected-component path marked every voxel of a 2D image as a boundary voxel because it tested the unit axis, so no hole was filled. The BFS path already ignored unit axes. The boundary test now ignores an axis of size 1. Add bool and uint8 tests for all three 2D orientations; they fail on the old code. Signed-off-by: Michael Jackson --- .../SimplnxCore/docs/IdentifySampleFilter.md | 4 +- .../Filters/Algorithms/IdentifySampleCCL.cpp | 4 +- .../SimplnxCore/test/IdentifySampleTest.cpp | 49 +++++++++++++++++++ 3 files changed, 54 insertions(+), 3 deletions(-) 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/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/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(); From 79962a84ca3c2760bb20d7da1db6b000924706fc Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Sat, 24 Oct 2026 09:30:56 -0400 Subject: [PATCH 81/91] DOC: Add the V&V recertification policy Describe when V&V work happens after a filter has a V&V record, so that engineers and their tools apply one approach: - Proposed trigger: a change that alters output for valid input needs a scoped V&V update in the same PR; other changes need only unchanged V&V tests; a release gate runs the legacy comparisons again. - Agreed: all V&V work stays on one branch until the policy and the report template are stable, with code fixes in separate commits. - Agreed: the three-way legacy comparison packages (stock DREAM.3D v6.5.171, a local proof version, DREAM3D-NX) and their archive rules. Link the new file from vv_policy.md. Signed-off-by: Michael Jackson --- docs/vv_templates/vv_policy.md | 5 +++ docs/vv_templates/vv_recertification.md | 48 +++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 docs/vv_templates/vv_recertification.md diff --git a/docs/vv_templates/vv_policy.md b/docs/vv_templates/vv_policy.md index a58b5275c6..6865a82866 100644 --- a/docs/vv_templates/vv_policy.md +++ b/docs/vv_templates/vv_policy.md @@ -64,6 +64,11 @@ The verified state is pinned by **(commit hash, archive SHA512)**. The commit ca | [`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 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 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". From f0d4661293659b6843d121714b348b5928b1e557 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Sat, 24 Oct 2026 09:40:41 -0400 Subject: [PATCH 82/91] DOC: Add the cell tuple-count check to the BadDataNeighborOrientationCheck V&V record Signed-off-by: Michael Jackson --- .../vv/BadDataNeighborOrientationCheckFilter.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Plugins/OrientationAnalysis/vv/BadDataNeighborOrientationCheckFilter.md b/src/Plugins/OrientationAnalysis/vv/BadDataNeighborOrientationCheckFilter.md index 3ea52bf341..cd9aa50810 100644 --- a/src/Plugins/OrientationAnalysis/vv/BadDataNeighborOrientationCheckFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/BadDataNeighborOrientationCheckFilter.md @@ -16,8 +16,8 @@ |--------|---------------| | 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 | 17 of 28 exercised. One additional path has a fixture; ten paths have no direct fixtures. | -| Tests today | 34 ordinary test cases and 2 hidden template variants cover masks, propagation, invariants, input validation and SIMPL conversion. | +| 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. | @@ -57,9 +57,9 @@ All defects in this table are fixed. The fixes will be in the DREAM3D-NX release ## Code path coverage -17 of 28 paths exercised. Path 28 has an added fixture. The table maps source paths to existing fixtures and gives the reason for each path without a fixture. +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` (180 lines). +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. @@ -93,6 +93,7 @@ Worklist counts matching neighbors and propagates changes through a queue. Scanl | 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 @@ -132,6 +133,7 @@ Worklist counts matching neighbors and propagates changes through a queue. Scanl | `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. | From 88b039576e481b1915dfc009c8ac668a229226db Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Sat, 24 Oct 2026 09:40:41 -0400 Subject: [PATCH 83/91] DOC: Add the cell tuple-count check to the ComputeFeatureReferenceCAxisMisorientations V&V record Signed-off-by: Michael Jackson --- ...tureReferenceCAxisMisorientationsFilter.md | 31 ++++++++++++------- ...tureReferenceCAxisMisorientationsFilter.md | 2 +- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/Plugins/OrientationAnalysis/vv/ComputeFeatureReferenceCAxisMisorientationsFilter.md b/src/Plugins/OrientationAnalysis/vv/ComputeFeatureReferenceCAxisMisorientationsFilter.md index b9f95dbc20..af7e2f5728 100644 --- a/src/Plugins/OrientationAnalysis/vv/ComputeFeatureReferenceCAxisMisorientationsFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/ComputeFeatureReferenceCAxisMisorientationsFilter.md @@ -16,8 +16,8 @@ |--------|---------------| | 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 | 16 of 27 exercised by the visible tests. Gaps include Hexagonal-Low, angles above 90°, transfer failures and cancellation. | -| Tests today | 9 test cases: 8 visible and 1 hidden slice-boundary case. The visible cases check analytical values, invalid inputs and SIMPL conversion. | +| 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 | @@ -25,7 +25,7 @@ ## Summary -`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 retains the legacy non-hexagonal correction and fixes two SIMPLNX defects. +`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 @@ -55,9 +55,9 @@ All defects in this table are fixed. The fixes will be in the DREAM3D-NX release ## Code path coverage -16 of 27 paths exercised. The count uses the eight visible tests; the table lists the remaining paths and their gaps. +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` (277 lines); `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeFeatureReferenceCAxisMisorientationsFilter.cpp` (198 lines). +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)`. @@ -65,35 +65,37 @@ Test names below omit `OrientationAnalysis::ComputeFeatureReferenceCAxisMisorien |---|-------|------|-----------| | 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, not the exact code | +| 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 | *Not directly tested. No analytical fixture selects Hexagonal-Low.* | +| 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 or negative FeatureId → skip the angle calculation | *Not directly tested. The skip fixtures use a negative phase or feature zero.* | +| 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 by FeatureId | All three Class 1 cases | +| 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 | *Not directly tested. The only fixture for this path is the hidden `genuine HDF5 slice-boundary oracle`.* | +| 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 or cell-angle read fails → return error | *Not directly tested. No fixture injects a second-pass read 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 -The eight visible cases are in `src/Plugins/OrientationAnalysis/test/ComputeFeatureReferenceCAxisMisorientationsTest.cpp`. All pass in CI. The table also includes the hidden case. +The 13 visible cases and one hidden case are in `src/Plugins/OrientationAnalysis/test/ComputeFeatureReferenceCAxisMisorientationsTest.cpp`. | Test case | Status | Notes | |-----------|--------|-------| @@ -106,6 +108,11 @@ The eight visible cases are in `src/Plugins/OrientationAnalysis/test/ComputeFeat | `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 diff --git a/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFeatureReferenceCAxisMisorientationsFilter.md b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFeatureReferenceCAxisMisorientationsFilter.md index b439950dfb..007f2a01d8 100644 --- a/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFeatureReferenceCAxisMisorientationsFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFeatureReferenceCAxisMisorientationsFilter.md @@ -40,7 +40,7 @@ DREAM.3D v6.5.171 also replaces a zero-count feature mean with zero. Its standar **Symptom:** Cell angles and feature statistics can differ numerically from DREAM.3D v6.5.171 because intermediate precision and evaluation order differ. -**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 remains float32. Its variance accumulator is double, although subtraction of the stored cell angle and feature mean occurs in float32 before assignment to double. +**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. 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. From 910bb2367939d587ff184d8cb68d6b4593e674b7 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Sat, 24 Oct 2026 09:40:41 -0400 Subject: [PATCH 84/91] DOC: Add the cell tuple-count check to the ComputeKernelAvgMisorientations V&V record Signed-off-by: Michael Jackson --- .../vv/ComputeKernelAvgMisorientationsFilter.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Plugins/OrientationAnalysis/vv/ComputeKernelAvgMisorientationsFilter.md b/src/Plugins/OrientationAnalysis/vv/ComputeKernelAvgMisorientationsFilter.md index c5d448c007..bf7b19d68a 100644 --- a/src/Plugins/OrientationAnalysis/vv/ComputeKernelAvgMisorientationsFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/ComputeKernelAvgMisorientationsFilter.md @@ -16,8 +16,8 @@ |--------|---------------| | Algorithm Relationship | Minor changes to `FindKernelAvgMisorientations` in DREAM.3D v6.5.171. SIMPLNX corrects the X kernel bound, improves precision and adds an optional same-phase mode across feature boundaries. | | Oracle (confirmed) | Class 1 analytical averages and Class 4 invariants are encoded in `test/ComputeKernelAvgMisorientationsTest.cpp`. All pass in CI. | -| Code paths enumerated | 28 of 45 exercised. The table identifies gaps in fallback modes, defensive guards, cancellation and transfer failures. | -| Tests today | 17 test cases cover analytical averages, invariants, input bounds, memory planning and SIMPL conversion. All pass in CI. | +| Code paths enumerated | 29 of 46 exercised. The table identifies gaps in fallback modes, defensive guards, cancellation and transfer failures. | +| Tests today | 18 test cases cover analytical averages, invariants, input bounds, geometry cell tuple counts, memory planning and SIMPL conversion. All pass in CI. | | Exemplar archive | Current tests construct their inputs and expected values in C++. The former KAM exemplar in `6_6_stats_test_v2.tar.gz` is retired for this filter. | | Legacy comparison | Not run on the current algorithm. The DREAM.3D v6.5.171 comparison dated 2026-09-17 predates the current implementation. | | Bug flags | None. | @@ -56,9 +56,9 @@ All defects in this table are fixed. The fixes will be in the DREAM3D-NX release ## Code path coverage -28 of 45 paths exercised. Each uncovered row states the missing fixture or the guard that prevents direct execution. +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` (175 lines). +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. @@ -109,6 +109,7 @@ Test names below omit the `OrientationAnalysis::ComputeKernelAvgMisorientationsF | 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 @@ -133,6 +134,7 @@ All pass in CI. | `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 From 5f78c1a5e5b95ff1ebc733b66fe1948dc387d730 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Sat, 24 Oct 2026 09:40:41 -0400 Subject: [PATCH 85/91] DOC: Add the cell tuple-count check to the NeighborOrientationCorrelation V&V record Signed-off-by: Michael Jackson --- .../vv/NeighborOrientationCorrelationFilter.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Plugins/OrientationAnalysis/vv/NeighborOrientationCorrelationFilter.md b/src/Plugins/OrientationAnalysis/vv/NeighborOrientationCorrelationFilter.md index a087eab449..698596689f 100644 --- a/src/Plugins/OrientationAnalysis/vv/NeighborOrientationCorrelationFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/NeighborOrientationCorrelationFilter.md @@ -16,8 +16,8 @@ |--------|---------------| | 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 | 26 of 40 exercised. Gaps include transfer-order cases, cancellation, and I/O failures. | -| Tests today | 19 ordinary test cases cover analytical outputs, invariants, invalid inputs and SIMPL conversion. All pass in CI; 1 additional hidden case generates input data. | +| 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 | @@ -58,9 +58,9 @@ All defects in this table are fixed. DREAM3D-NX v7.4.2 contains the fixes for D2 ## Code path coverage -26 of 40 paths exercised. Each uncovered path states its missing fixture below. +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` (243 lines), and `src/simplnx/Utilities/SliceBufferedTransfer.hpp` (364 lines). +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. @@ -106,10 +106,11 @@ Test names below omit the common `OrientationAnalysis::NeighborOrientationCorrel | 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`. The 19 ordinary cases are the CI validation set. All pass in CI. The hidden data generator is listed separately from correctness checks. +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 | |-----------|--------|-------| @@ -133,6 +134,7 @@ All names are in `src/Plugins/OrientationAnalysis/test/NeighborOrientationCorrel | `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 From 4ca836c30cd0246547cf903271a0031d84ed8fdb Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Sat, 24 Oct 2026 09:40:41 -0400 Subject: [PATCH 86/91] DOC: Add the cell tuple-count check to the ComputeFeatureCentroids V&V record Signed-off-by: Michael Jackson --- .../SimplnxCore/vv/ComputeFeatureCentroidsFilter.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Plugins/SimplnxCore/vv/ComputeFeatureCentroidsFilter.md b/src/Plugins/SimplnxCore/vv/ComputeFeatureCentroidsFilter.md index df205a3975..607bc4b94d 100644 --- a/src/Plugins/SimplnxCore/vv/ComputeFeatureCentroidsFilter.md +++ b/src/Plugins/SimplnxCore/vv/ComputeFeatureCentroidsFilter.md @@ -16,8 +16,8 @@ |--------|---------------| | 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 | 16 of 26 exercised by the five ordinary test cases. The table identifies validation, cancellation, I/O and periodic-axis gaps. | -| Tests today | 6 test cases: analytical centroids, periodic centroids, invalid IDs, bulk reads, SIMPL conversion and a hidden storage-contract fixture. | +| 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. | @@ -54,9 +54,9 @@ All defects in this table are fixed. DREAM3D-NX v7.4.2 contains the fix for `Com ## Code path coverage -16 of 26 paths exercised. The count uses the five ordinary CI test cases; the hidden storage-contract fixture is listed separately in the inventory. +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` (165 lines), and `src/simplnx/Utilities/DataArrayUtilities.cpp` (515 lines; shared FeatureIds validation). +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 | |---|-------|------|-----------| @@ -86,10 +86,11 @@ Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeFeatu | 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 five ordinary test cases run in in-core CI on Linux, macOS and Windows. All pass in CI. +The inventory contains six ordinary test cases and one hidden storage-contract fixture. | Test case | Status | Notes | |-----------|--------|-------| @@ -99,6 +100,7 @@ The five ordinary test cases run in in-core CI on Linux, macOS and Windows. All | `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 From a5d92984a87b89269ad91fff75a51026ba015050 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Sat, 24 Oct 2026 09:40:41 -0400 Subject: [PATCH 87/91] DOC: Add the cell tuple-count check to the ComputeFeatureNeighbors V&V record Signed-off-by: Michael Jackson --- .../SimplnxCore/vv/ComputeFeatureNeighborsFilter.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Plugins/SimplnxCore/vv/ComputeFeatureNeighborsFilter.md b/src/Plugins/SimplnxCore/vv/ComputeFeatureNeighborsFilter.md index 06ed25b2d0..fcdbb31c0b 100644 --- a/src/Plugins/SimplnxCore/vv/ComputeFeatureNeighborsFilter.md +++ b/src/Plugins/SimplnxCore/vv/ComputeFeatureNeighborsFilter.md @@ -16,8 +16,8 @@ |--------|---------------| | 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 | 24 of 34 exercised by the mapped tests. Error returns, cancellation, negative IDs and the unreachable dispatch fallback have no direct fixture. | -| Tests today | 38 test cases: 35 analytical cases, one archive regression, one SIMPL conversion case and one hidden HDF5 case. The analytical cases cover eight dimensionality states and all four optional-output combinations. | +| 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. | @@ -54,9 +54,9 @@ All defects in this table are fixed. DREAM3D-NX v7.4.2 contains the fixes for `C ## Code path coverage -24 of 34 paths exercised. Each uncovered path states why it has no direct fixture. +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` (223 lines), and `src/simplnx/Utilities/NeighborUtilities.hpp` (634 lines). +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. @@ -96,10 +96,11 @@ Test names below omit the common `SimplnxCore::ComputeFeatureNeighborsFilter:` p | 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 -The 37 ordinary test cases cover analytical expectations, archive regression and conversion. All pass in CI. +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 | |-----------|--------|-------| @@ -141,6 +142,7 @@ The 37 ordinary test cases cover analytical expectations, archive regression and | `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 From 9a74b9dd3b5bb4c6ef4b6ac342693d71f2b0f1c9 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Sat, 24 Oct 2026 09:40:41 -0400 Subject: [PATCH 88/91] DOC: Add the cell tuple-count check to the ComputeFeatureSizes V&V record Signed-off-by: Michael Jackson --- .../SimplnxCore/vv/ComputeFeatureSizesFilter.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Plugins/SimplnxCore/vv/ComputeFeatureSizesFilter.md b/src/Plugins/SimplnxCore/vv/ComputeFeatureSizesFilter.md index e3e52205fe..4e4dd9a564 100644 --- a/src/Plugins/SimplnxCore/vv/ComputeFeatureSizesFilter.md +++ b/src/Plugins/SimplnxCore/vv/ComputeFeatureSizesFilter.md @@ -16,8 +16,8 @@ |--------|---------------| | 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 | 24 of 44 paths exercised with test assertions. Gaps include input read errors, count overflow, and several cancellation paths. | -| Tests today | 16 test cases: 14 visible and 2 hidden OOC cases. Coverage includes analytical values, output writes, invalid inputs, archive regression, and SIMPL conversion. | +| 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` | @@ -54,9 +54,9 @@ All defects in this table are fixed. The fixes will be in the DREAM3D-NX release ## Code path coverage -24 of 44 paths exercised with test assertions. Gaps include input read errors, count overflow, and cancellation during input processing. +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` (199 lines). +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. @@ -106,6 +106,7 @@ Test names below omit the `SimplnxCore::ComputeFeatureSizes: ` prefix, except wh | 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 @@ -129,6 +130,7 @@ All names are in `src/Plugins/SimplnxCore/test/ComputeFeatureSizesTest.cpp`. Dyn | `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 From 0f833ef343c02e22c664e0f728c431ed519140b3 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Sat, 24 Oct 2026 09:40:41 -0400 Subject: [PATCH 89/91] DOC: Add the cell tuple-count check to the ErodeDilateMask V&V record Signed-off-by: Michael Jackson --- src/Plugins/SimplnxCore/vv/ErodeDilateMaskFilter.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Plugins/SimplnxCore/vv/ErodeDilateMaskFilter.md b/src/Plugins/SimplnxCore/vv/ErodeDilateMaskFilter.md index c8423d4d0f..e7d22dbb7d 100644 --- a/src/Plugins/SimplnxCore/vv/ErodeDilateMaskFilter.md +++ b/src/Plugins/SimplnxCore/vv/ErodeDilateMaskFilter.md @@ -18,8 +18,8 @@ |--------|---------------| | Algorithm Relationship | Port of `ErodeDilateMask` in DREAM.3D v6.5.171. SIMPLNX uses rolling slice buffers for the same synchronous face-neighbour operations. | | Oracle (confirmed) | Class 1 analytical masks are encoded in 8 test cases in `test/ErodeDilateMaskTest.cpp`. All pass in CI. | -| Code paths enumerated | 18 of 26 exercised by the mapped test cases. All-directions-off, six I/O failure paths and external-store execution lack direct tests in this file. | -| Tests today | 13 test cases: 8 analytical, 2 exemplar comparisons, 1 validation, 1 SIMPL conversion and 1 hidden data generator. | +| Code paths enumerated | 19 of 27 exercised by the mapped test cases. All-directions-off, six I/O failure paths and external-store execution lack direct tests in this file. | +| Tests today | 14 test cases: 8 analytical, 2 exemplar comparisons, 2 validation, 1 SIMPL conversion and 1 hidden data generator. | | Exemplar archive | `6_6_erode_dilate_test.tar.gz` supplies inputs and expected outputs for the two exemplar comparisons. It does not establish parity with DREAM.3D v6.5.171. | | Legacy comparison | Not run for the current algorithm. The recorded comparison with DREAM.3D v6.5.171 predates the change to rolling slice buffers. | | Bug flags | None. | @@ -56,9 +56,9 @@ All defects in this table are fixed. The fixes will be in the DREAM3D-NX release ## Code path coverage -18 of 26 paths exercised. The uncovered rows identify the remaining gaps. +19 of 27 paths exercised. The uncovered rows identify the remaining gaps. -Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ErodeDilateMask.cpp` (247 lines), `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ErodeDilateMaskFilter.cpp` (164 lines), and `src/simplnx/Utilities/NeighborUtilities.hpp` (634 lines). +Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ErodeDilateMask.cpp` (255 lines), `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ErodeDilateMaskFilter.cpp` (171 lines), and `src/simplnx/Utilities/NeighborUtilities.hpp` (634 lines). Test names below omit the common `SimplnxCore::ErodeDilateMaskFilter` prefix. The phases are setup, scan, I/O and preflight. @@ -90,6 +90,7 @@ Test names below omit the common `SimplnxCore::ErodeDilateMaskFilter` prefix. Th | 24 | I/O | Propagate failure while flushing a single-slice image | *Not directly tested. Requires an injected store write failure.* | | 25 | I/O | Propagate failure while flushing the last multi-slice image slice | *Not directly tested. Requires an injected store write failure.* | | 26 | I/O | Read and write a mask held in an external store | *Not directly tested. No case in this file requires an external mask store.* | +| 27 | Preflight | Reject a mask tuple count that differs from the selected geometry's cell count with `-14702` | `Geometry cell tuple count` — 0, 11 and 13 tuples for 12 cells | ## Test inventory @@ -108,6 +109,7 @@ Test names below omit the common `SimplnxCore::ErodeDilateMaskFilter` prefix. Th | `SimplnxCore::ErodeDilateMaskFilter: Class 1 erode honours direction flags` | new-for-V&V | Checks all 27 hollow-cube cells in `All directions on` and `Z direction off`: 20 and 22 cells remain true. Covers `ErodeDilateMaskFilter-D2`. | | `SimplnxCore::ErodeDilateMaskFilter: Invalid Number of Iterations` | new-for-V&V | Checks invalid preflight and error `-14701` for 0 and -2. Covers `ErodeDilateMaskFilter-D1`. | | `SimplnxCore::ErodeDilateMaskFilter: SIMPL Backwards Compatibility` | kept | `SIMPL 6.5 (UUID)` and `SIMPL 6.4 (Filter_Name)` sections check the converted UUID and all arguments; they do not execute the filter. | +| `SimplnxCore::ErodeDilateMaskFilter: Geometry cell tuple count` | new-for-V&V | Checks preflight error `-14702` for 0, 11 and 13 mask tuples against 12 cells, with the array path and both counts in the message. Accepts 12 tuples in another Attribute Matrix with a different tuple shape. | ## Exemplar archive From bb92649eab96c04480624a1bddd011772e9e77ec Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Sat, 24 Oct 2026 09:40:41 -0400 Subject: [PATCH 90/91] DOC: Add the cell tuple-count check to the IdentifySample V&V record Signed-off-by: Michael Jackson --- .../SimplnxCore/vv/IdentifySampleFilter.md | 30 +++++++++++-------- .../vv/deviations/IdentifySampleFilter.md | 14 ++++----- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/src/Plugins/SimplnxCore/vv/IdentifySampleFilter.md b/src/Plugins/SimplnxCore/vv/IdentifySampleFilter.md index 6051febbc7..e37439c401 100644 --- a/src/Plugins/SimplnxCore/vv/IdentifySampleFilter.md +++ b/src/Plugins/SimplnxCore/vv/IdentifySampleFilter.md @@ -16,16 +16,16 @@ |--------|---------------| | Algorithm Relationship | Rewrite of `IdentifySample` in DREAM.3D v6.5.171. BFS retains the two-pass search; CCL uses component labels and replay, and SIMPLNX adds slice processing. | | Oracle (confirmed) | Class 1 masks in the three non-square cases in `test/IdentifySampleTest.cpp` check largest-component selection. All pass in CI. | -| Code paths enumerated | 21 of 44 exercised. Gaps include flat-image hole filling, ties, cancellation, and storage failures. | -| Tests today | 6 test cases: 1 archive case with 12 sections, 3 analytical cases, 1 conversion case, and 1 hidden HDF5 case. The five ordinary cases all pass in CI. | +| Code paths enumerated | 26 of 45 exercised. Gaps include one-dimensional images, ties, cancellation, and storage failures. | +| Tests today | 8 test definitions produce 9 cases, including the bool and uint8 template cases. Coverage includes archive masks, analytical masks, tuple counts, conversion, and hidden HDF5 storage. | | Exemplar archive | `identify_sample_v2.tar.gz` supplies inputs and SIMPLNX-generated regression outputs. Inline analytical masks provide the independent oracle. | | Legacy comparison | Not run — comparison artifacts are not available. | -| Bug flags | `IdentifySampleFilter-D6` | +| Bug flags | None. | | V&V phase | Source review, analytical fixtures, and archive regression are complete. Second-engineer oracle review and a current legacy comparison remain open. | ## Summary -`IdentifySampleFilter` keeps the largest face-connected mask component and can fill enclosed holes, either for the whole image or for each selected slice. Class 1 masks check connectivity, and twelve archive sections check regression behavior. The fixed defects are listed below; CCL still leaves enclosed holes unfilled on flat images (`IdentifySampleFilter-D6`). +`IdentifySampleFilter` keeps the largest face-connected mask component and can fill enclosed holes, either for the whole image or for each selected slice. Class 1 masks check connectivity, and twelve archive sections check regression behavior. The fixed defects are listed below; BFS and CCL ignore flat axes when they check whether a hole reaches the image boundary. ## Algorithm Relationship @@ -45,7 +45,7 @@ ## Bugs found and fixed -All defects in this table are fixed. DREAM3D-NX v7.4.2 contains the BFS fix for `IdentifySample-D1` and the fixes for `IdentifySampleFilter-D2` and `IdentifySampleFilter-D3`. DREAM3D-NX v7.4.1 contains the fixes for `IdentifySampleFilter-D4` and `IdentifySampleFilter-D5`. +All defects in this table are corrected in the current source. DREAM3D-NX v7.4.2 contains the BFS fix for `IdentifySample-D1` and the fixes for `IdentifySampleFilter-D2` and `IdentifySampleFilter-D3`. DREAM3D-NX v7.4.1 contains the fixes for `IdentifySampleFilter-D4` and `IdentifySampleFilter-D5`. | Deviation | Defect | Affected released versions | Resolution | |-----------|--------|----------------------------|------------| @@ -54,12 +54,13 @@ All defects in this table are fixed. DREAM3D-NX v7.4.2 contains the BFS fix for | `IdentifySampleFilter-D3` | Slice processing kept the first of two equally large components, unlike whole-image BFS. | DREAM3D-NX v7.0.2 through v7.4.1. DREAM.3D v6.5.171 was not affected. | The slice BFS uses `>=` and keeps the later component on a tie. | | `IdentifySampleFilter-D4` | Slice hole filling used a local plane index as a global index for XZ and YZ planes. | DREAM3D-NX v7.0.2 through v7.4.0. DREAM.3D v6.5.171 was not affected. | Plane coordinates and strides place each filled voxel in the selected slice. | | `IdentifySampleFilter-D5` | Slice hole filling reused visited flags from component selection and skipped removed internal islands. | DREAM3D-NX v7.0.2 through v7.4.0. DREAM.3D v6.5.171 was not affected. | Slice BFS clears its visited flags before hole filling. CCL labels the false components in a separate pass. | +| `IdentifySampleFilter-D6` | Whole-image CCL treated a flat axis as a boundary at every cell and left enclosed holes unfilled. | None. DREAM3D-NX v7.0.0 through v7.4.2 do not contain CCL. DREAM.3D v6.5.171 has the same boundary-rule defect. | CCL ignores flat axes when it marks boundary roots. The unit-dimensions test checks all three flat orientations. | ## Code path coverage -21 of 44 paths exercised. The table separates test assertions from paths that have no direct fixture. +26 of 45 paths exercised. The table maps current test assertions to paths; row 42 has partial coverage. -Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/IdentifySample.cpp` (28 lines); `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/IdentifySampleBFS.cpp` (295 lines); `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/IdentifySampleCCL.cpp` (1055 lines); `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/IdentifySampleCommon.hpp` (517 lines); `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/IdentifySampleFilter.cpp` (139 lines). +Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/IdentifySample.cpp` (28 lines); `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/IdentifySampleBFS.cpp` (295 lines); `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/IdentifySampleCCL.cpp` (1057 lines); `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/IdentifySampleCommon.hpp` (517 lines); `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/IdentifySampleFilter.cpp` (146 lines). `Archive` below means `SimplnxCore::IdentifySampleFilter`. Other names omit the `SimplnxCore::IdentifySampleFilter: ` prefix. The ordinary mask cases select BFS and CCL on resident stores through the scenario harness. @@ -73,13 +74,13 @@ Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/IdentifySamp | 6 | BFS dispatch | X-only line. | *Not directly tested. No one-dimensional fixture exists.* | | 7 | BFS dispatch | Y-only line. | *Not directly tested. No one-dimensional fixture exists.* | | 8 | BFS dispatch | Z-only line. | *Not directly tested. No one-dimensional fixture exists.* | -| 9 | BFS dispatch | Single voxel, including its hole-fill boundary guard. | *Not directly tested. No single-voxel fixture exists.* | +| 9 | BFS dispatch | Single voxel, including its hole-fill boundary guard. | `unit dimensions hole filling` — the false 1×1×1 voxel stays false with filling off and on. | | 10 | BFS selection | Expand through valid true neighbors; skip invalid, false, and visited neighbors. | Archive; all three non-square cases. | | 11 | BFS selection | Keep the largest component and clear smaller components. | Archive; all three non-square cases. | | 12 | BFS filling | `FillHoles=false`. | Archive — `whole_*_nofill`; all three non-square cases. | | 13 | BFS filling | Fill an enclosed 3D false component after resetting visited flags. | Archive — `whole_*_fill`, including the internal island. | | 14 | BFS filling | Leave a boundary-connected false component unchanged. | Archive — `whole_*_fill`. | -| 15 | BFS filling | Fill an enclosed hole in a flat image. | *Not directly tested. The non-square fixtures disable hole filling.* | +| 15 | BFS filling | Fill an enclosed hole in a flat image. | `unit dimensions hole filling` — all three flat orientations, bool and uint8 masks. | | 16 | BFS selection | Equal-size whole-image components select the later component. | *Not directly tested. No fixture isolates an equal-size largest-component tie.* | | 17 | Slice BFS | XY plane, filling disabled and enabled; contiguous plane transfers. | Archive — `sliced_xy_nofill`, `sliced_xy_fill`. | | 18 | Slice BFS | XZ plane, filling disabled and enabled; contiguous row transfers. | Archive — `sliced_xz_nofill`, `sliced_xz_fill`. | @@ -92,11 +93,11 @@ Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/IdentifySamp | 25 | Slice CCL | XY planes with filling disabled and enabled. | Archive — `sliced_xy_nofill`, `sliced_xy_fill`. | | 26 | Slice CCL | XZ planes with filling disabled and enabled. | Archive — `sliced_xz_nofill`, `sliced_xz_fill`. | | 27 | Slice CCL | YZ planes with filling disabled and enabled. | Archive — `sliced_yz_nofill`, `sliced_yz_fill`. | -| 28 | CCL selection | Whole image has no true component; return without replay. | *Not directly tested. No all-false whole-image fixture exists.* | +| 28 | CCL selection | Whole image has no true component; return without replay. | `unit dimensions hole filling` — the all-false 1×1×1 image stays false. | | 29 | Slice CCL | An empty plane returns without component removal. | Archive — slices outside all documented features. | | 30 | CCL selection | Create labels, merge connected labels, and replay to remove smaller roots. | Archive; all three non-square cases. | | 31 | CCL filling | Label false components, mark boundary roots, and fill enclosed roots. | Archive — `whole_*_fill` and `sliced_*_fill`. | -| 32 | CCL filling | A flat whole image marks every false root as boundary-connected. | *Not directly tested. Flat fixtures disable hole filling; see `IdentifySampleFilter-D6`.* | +| 32 | CCL filling | Ignore flat axes when marking boundary roots; fill enclosed holes and preserve boundary-connected false cells. | `unit dimensions hole filling` — all three flat orientations, bool and uint8 masks, filling off and on. | | 33 | CCL storage | HDF5 mask and equivalences cross a 4096-record page boundary. | *Not directly tested. The hidden `genuine HDF5 equivalence-page and slice oracle` requires a real HDF5 store; the supplied CI evidence covers in-core builds.* | | 34 | CCL storage | A failed temporary-store request falls back to resident records. | *Not directly tested. No fixture isolates provider failure and fallback.* | | 35 | CCL storage | Provider failure without fallback, or null store (`-45460`). | *Not directly tested. No fixture injects these provider responses.* | @@ -106,13 +107,14 @@ Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/IdentifySamp | 39 | CCL storage | Equivalence or root-flag creation, access, or flush fails. | *Not directly tested. No temporary store injects these failures.* | | 40 | Cancellation | Return during BFS scanning, slice processing, CCL labeling, or replay. | *Not directly tested. No fixture injects cancellation.* | | 41 | CCL selection | Equal-size roots select the larger root label. | *Not directly tested. No fixture isolates a tie between largest roots.* | -| 42 | CCL dispatch | One-dimensional or single-voxel whole image. | *Not directly tested. The smallest flat fixture is a 3×4 plane.* | +| 42 | CCL dispatch | One-dimensional or single-voxel whole image. | Partial: `unit dimensions hole filling` checks the false single voxel. *One-dimensional images are not directly tested; no line fixture exists.* | | 43 | Slice CCL | Boolean masks in XY, XZ, or YZ planes. | *Not directly tested. All slice fixtures use uint8 masks.* | | 44 | Slice BFS | Boolean masks in XY or XZ planes. | *Not directly tested. All slice fixtures use uint8 masks.* | +| 45 | Preflight | Reject a mask whose tuple count differs from the selected geometry cell count (`-45463`). | `Geometry cell tuple count` — rejects 0, 11, and 13 tuples for 12 cells; accepts 12 tuples in another Attribute Matrix. | ## Test inventory -The five ordinary cases in `src/Plugins/SimplnxCore/test/IdentifySampleTest.cpp` run in the supplied in-core CI configurations. All pass in CI. +`src/Plugins/SimplnxCore/test/IdentifySampleTest.cpp` contains seven ordinary test definitions and one hidden HDF5 case. The template definition produces bool and uint8 cases. The supplied in-core CI evidence covers the archive case, the three non-square cases, and the conversion case; all five pass. | Test case | Status | Notes | |-----------|--------|-------| @@ -122,6 +124,8 @@ The five ordinary cases in `src/Plugins/SimplnxCore/test/IdentifySampleTest.cpp` | `SimplnxCore::IdentifySampleFilter: 2D Empty Z Non-Square {3,4,1}` | kept | Checks all 12 bool values against the four-cell analytical component in the XY plane. | | `SimplnxCore::IdentifySampleFilter: 2D Empty Y Non-Square {3,1,4}` | kept | Checks the same analytical component in the XZ plane. | | `SimplnxCore::IdentifySampleFilter: 2D Empty X Non-Square {1,3,4}` | kept | Checks the same analytical component in the YZ plane. | +| `SimplnxCore::IdentifySampleFilter: unit dimensions hole filling` | new-for-V&V | Template cases for bool and uint8 check every value on 5×5×1, 5×1×5, 1×5×5, and 1×1×1 images with filling off and on. Enclosed centers fill only when enabled; a false corner and the lone false voxel stay false. | +| `SimplnxCore::IdentifySampleFilter: Geometry cell tuple count` | new-for-V&V | Preflight rejects masks with 0, 11, or 13 tuples for a 12-cell geometry with error `-45463`. Checks the array path and tuple/cell counts in the message; accepts 12 tuples in another Attribute Matrix. | ## Exemplar archive diff --git a/src/Plugins/SimplnxCore/vv/deviations/IdentifySampleFilter.md b/src/Plugins/SimplnxCore/vv/deviations/IdentifySampleFilter.md index ca34e9a8c3..47b168a69e 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/IdentifySampleFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/IdentifySampleFilter.md @@ -46,7 +46,7 @@ Part 1 records the difference from DREAM.3D v6.5.171. Part 2 records SIMPLNX def **Recommendation:** Trust SIMPLNX. A single false voxel stays false because it touches the image boundary. -**Evidence:** PR #1640 (`a332e7369`) adds the guard after PR #1590 introduced dimensional dispatch. Releases through v7.4.1 have coordinate boundary checks; v7.4.2 has the guard. DREAM.3D v6.5.171 also has coordinate checks. Current `IdentifySampleBFS.cpp` retains the guard; no test in `IdentifySampleTest.cpp` constructs a single-voxel image. +**Evidence:** PR #1640 (`a332e7369`) adds the guard after PR #1590 introduced dimensional dispatch. Releases through v7.4.1 have coordinate boundary checks; v7.4.2 has the guard. DREAM.3D v6.5.171 also has coordinate checks. Current `IdentifySampleBFS.cpp` retains the guard. `SimplnxCore::IdentifySampleFilter: unit dimensions hole filling` checks that a false single voxel stays false with filling off and on for bool and uint8 masks. --- @@ -124,14 +124,14 @@ Part 1 records the difference from DREAM.3D v6.5.171. Part 2 records SIMPLNX def | **Filter UUID** | `94d47495-5a89-4c7f-a0ee-5ff20e6bd273` | | **Status** | active | | **Affected released versions** | None. DREAM3D-NX v7.0.0 through v7.4.2 do not contain CCL. DREAM.3D v6.5.171 was affected by the same boundary rule. | -| **Fixed in** | Not fixed | +| **Fixed in** | The DREAM3D-NX release after v7.4.2 | -**Symptom:** With whole-image processing and hole filling enabled, CCL leaves enclosed holes false on a 2D image. BFS fills them. +**Symptom:** With whole-image processing and hole filling enabled, CCL left enclosed holes false on a 2D image. BFS filled them. -**Root cause:** Bug. CCL marks a false component as exterior when any voxel lies at an extremum of any of the three axes. A flat axis therefore marks every false component as exterior. BFS excludes flat axes from its neighbor boundary check. This CCL regression restores the boundary rule documented in `IdentifySample-D1`. +**Root cause:** Bug. CCL marked a false component as exterior when any voxel was at an extremum of any of the three axes. A flat axis therefore marked every false component as exterior. CCL now excludes flat axes from this check, as BFS does. This corrects the boundary-rule defect documented in `IdentifySample-D1`. -**Affected users:** Users whose whole-image execution selects CCL for a flat image with an enclosed hole. Slice processing uses a separate boundary test. +**Affected users:** Users whose whole-image execution selected CCL for a flat image with an enclosed hole before the correction. Slice processing uses a separate boundary test. -**Recommendation:** Use BFS for whole-image 2D hole filling, or select slice processing in the non-flat plane. Do not assume whole-image BFS and CCL give the same filled mask on a flat image. +**Recommendation:** Use the corrected CCL implementation or BFS for whole-image 2D hole filling. Recalculate affected masks. -**Evidence:** Current `IdentifySampleCCL.cpp` marks boundary roots with `x == 0 || x == dimX - 1 || y == 0 || y == dimY - 1 || z == 0 || z == dimZ - 1`. The behavior follows directly for a 5×5×1 true mask with a false center: `z == 0` marks that center as exterior. The filter dispatcher does not exclude flat images from CCL. PR #1568 (`4ec21e93f`) adds CCL after v7.4.2. All listed release sources contain only the earlier algorithm. The non-square tests disable hole filling, and the hidden HDF5 test uses a 129×129×3 image, so neither detects this case. +**Evidence:** Current `IdentifySampleCCL.cpp` tests coordinate extrema only on axes with more than one cell. `SimplnxCore::IdentifySampleFilter: unit dimensions hole filling` checks every mask value in all three 5×5 flat orientations for bool and uint8 masks, with filling off and on. It checks that the enclosed center fills only when enabled and the false corner stays false. It also checks a false 1×1×1 image, which returns before CCL hole filling because it has no true component. DREAM3D-NX v7.0.0 through v7.4.2 do not contain CCL. From 1b032d602ea38f021981d3f193b6b640a4c2d557 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Sat, 24 Oct 2026 09:40:41 -0400 Subject: [PATCH 91/91] DOC: Add the cell tuple-count check to the ReplaceElementAttributesWithNeighborValues V&V record Signed-off-by: Michael Jackson --- ...placeElementAttributesWithNeighborValuesFilter.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Plugins/SimplnxCore/vv/ReplaceElementAttributesWithNeighborValuesFilter.md b/src/Plugins/SimplnxCore/vv/ReplaceElementAttributesWithNeighborValuesFilter.md index 22dc2619aa..ea1048c0e1 100644 --- a/src/Plugins/SimplnxCore/vv/ReplaceElementAttributesWithNeighborValuesFilter.md +++ b/src/Plugins/SimplnxCore/vv/ReplaceElementAttributesWithNeighborValuesFilter.md @@ -16,8 +16,8 @@ |--------|---------------| | Algorithm Relationship | Minor changes to `ReplaceElementAttributesWithNeighborValues` in DREAM.3D v6.5.171. SIMPLNX retains the face-neighbor rules, preserves the comparison value's type, and transfers numeric tuples one Z slice at a time. | | Oracle (confirmed) | Classes 1 and 4 use four synthetic sections in `test/ReplaceElementAttributesWithNeighborValuesTest.cpp`. All pass in CI. | -| Code paths enumerated | 20 of 30 paths exercised. Gaps include cancellation, I/O failures, nonnumeric siblings, other comparison types, and a loop with no passing values. | -| Tests today | 4 test cases: synthetic replacement, archive regression, SIMPL conversion, and a hidden data generator. The synthetic case has four sections. | +| Code paths enumerated | 21 of 31 paths exercised. Gaps include cancellation, I/O failures, nonnumeric siblings, other comparison types, and a loop with no passing values. | +| Tests today | 5 test cases: synthetic replacement, archive regression, SIMPL conversion, geometry cell tuple counts, and a hidden data generator. The synthetic case has four sections. | | Exemplar archive | `6_6_replace_element_attributes_with_neighbor.tar.gz` is used by the archive regression. The test loads the same file as input and reference; the independent oracle is in the synthetic test. | | Legacy comparison | Not run on the current algorithm. The comparison dated 2026-07-21 predates the change to slice transfers. | | Bug flags | `ReplaceElementAttributesWithNeighborValuesFilter-D3` | @@ -54,9 +54,9 @@ All defects in this table are fixed. DREAM3D-NX v7.4.2 contains both fixes. ## Code path coverage -20 of 30 paths exercised. Each uncovered path has a reason below. +21 of 31 paths exercised. Each uncovered path has a reason below. -Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReplaceElementAttributesWithNeighborValues.cpp` (368 lines); `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReplaceElementAttributesWithNeighborValuesFilter.cpp` (140 lines). +Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReplaceElementAttributesWithNeighborValues.cpp` (368 lines); `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReplaceElementAttributesWithNeighborValuesFilter.cpp` (147 lines). Rows that name a section refer to `SimplnxCore::ReplaceElementAttributesWithNeighborValuesFilter: Synthetic neighbor replacement`. Each pass selects neighbors from a rolling comparison window and transfers each completed Z slice. @@ -92,10 +92,11 @@ Rows that name a section refer to `SimplnxCore::ReplaceElementAttributesWithNeig | 28 | Dispatch | Compare boolean, integer, or float64 values instead of float32 | *Not directly tested. All executing test cases select a float32 comparison array.* | | 29 | Loop | Repeat without progress when every value fails the threshold | *Not directly tested. An uncancelled fixture would not terminate; see `ReplaceElementAttributesWithNeighborValuesFilter-D3`.* | | 30 | Preflight | Accept the comparison array and declare its parent contents modified | `Synthetic neighbor replacement` — preflight succeeds in all four sections | +| 31 | Preflight | Reject a comparison array whose tuple count differs from the selected geometry's cell count with error `-15001` | `SimplnxCore::ReplaceElementAttributesWithNeighborValuesFilter: Geometry cell tuple count` — 0, 11, and 13 tuples for 12 cells | ## Test inventory -The test cases are in `src/Plugins/SimplnxCore/test/ReplaceElementAttributesWithNeighborValuesTest.cpp`. The three ordinary cases pass in CI. The generator is hidden. +The test cases are in `src/Plugins/SimplnxCore/test/ReplaceElementAttributesWithNeighborValuesTest.cpp`. The archive regression, synthetic replacement, and SIMPL conversion cases pass in CI. The generator is hidden. | Test case | Status | Notes | |-----------|--------|-------| @@ -103,6 +104,7 @@ The test cases are in `src/Plugins/SimplnxCore/test/ReplaceElementAttributesWith | `SimplnxCore::ReplaceElementAttributesWithNeighborValuesFilter` | kept | Loads the same archive file as input and reference, runs LessThan at 0.1 with loop enabled, compares cell arrays, and checks tuple dimensions. This checks stability on the stored data. | | `SimplnxCore::ReplaceElementAttributesWithNeighborValuesFilter: Synthetic neighbor replacement` | new-for-V&V | Four sections check LessThan, GreaterThan, one pass, and a bad center surrounded by bad neighbors. Checks all 27 values against the threshold in the loop sections, exact replacement values, and a changed center marker. | | `SimplnxCore::ReplaceElementAttributesWithNeighborValuesFilter: SIMPL Backwards Compatibility` | kept | `SIMPL 6.5 (UUID)` and `SIMPL 6.4 (Filter_Name)` sections check identity, comments, threshold, comparison, loop, and both input paths. Does not execute replacement. | +| `SimplnxCore::ReplaceElementAttributesWithNeighborValuesFilter: Geometry cell tuple count` | new-for-V&V | Checks a 12-cell geometry with comparison arrays of 0, 11, 12, and 13 tuples in another AttributeMatrix. Accepts 12 tuples; otherwise requires one error with code `-15001` and a message that names the array path, tuple count, geometry, and cell count. | ## Exemplar archive