Skip to content

VV: Segment Features (C-Axis Misalignment) fully V&V'ed - #1685

Open
imikejackson wants to merge 12 commits into
BlueQuartzSoftware:developfrom
imikejackson:vv/c_axis_segmentation
Open

VV: Segment Features (C-Axis Misalignment) fully V&V'ed#1685
imikejackson wants to merge 12 commits into
BlueQuartzSoftware:developfrom
imikejackson:vv/c_axis_segmentation

Conversation

@imikejackson

@imikejackson imikejackson commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Full V&V cycle for CAxisSegmentFeaturesFilter per the v2 MTR V&V policy, verified against an independent Class 1 (Analytical) + Class 4 (Invariant) oracle before any legacy comparison.

  • Found and fixed 3 bugs, each pinned by a failing-first test:
    • D1 — the shared SegmentFeatures driver burst from an unvalidated raw seed = 0 (introduced in BUG: Fixes issue when segmenting features with a +1 feature count #1466): phantom empty feature / ids shifted by one / growth from a masked voxel whenever index 0 could not legitimately seed. Restored getSeed() for the first seed; fix also covers EBSDSegmentFeatures and ScalarSegmentFeatures, and each consumer suite now carries its own masked-voxel-0 regression pin.
    • D4 — the crystal-structure validation rejected datasets with unindexed (phase-0) or masked-out cells and indexed CrystalStructures without a bounds check (new -8364).
    • D5 — stale getDataAs<ImageGeom> cast crashed on RectGrid input (parameter allows RectGrid; siblings already used IGridGeometry).
  • Review hardening: -652/-653 preflight guards (cell arrays / cell AttributeMatrix vs geometry cell count), preflight-consistent FeatureIds path derivation and tuple shape, clean cancel return.
  • Retired the 4 circular-oracle exemplar tests (segment_features_test_data CAxis_FeatureIds_* arrays were generated from SIMPLNX output); the archive remains for the EBSD tests. Replaced with 20 tests built on inline pure-Phi Bunge fixtures with the closed-form derivation embedded in the test source.
  • Legacy 6.5.171 A/B comparison (official PipelineRunner vs nxrunner, shared legacy-format input): partition-level match on all 5 fixtures (incl. a 3x2x2 masked fixture covering the y/z stride branches) with identical feature counts; bit-identical FeatureIds are unattainable because 6.5.171 always clock-randomizes (D2). 5 deviations documented.
  • V&V deliverables: vv/CAxisSegmentFeaturesFilter.md (READY FOR REVIEW), vv/deviations/, vv/provenance/, vv/comparisons/ pipelines + results; user docs updated (hexagonal requirement, mask/phase-0 semantics, RectGrid, deterministic randomization).
  • vv_policy.md: "algorithmic choice" root-cause wording aligned with fleet practice (usable outside Rewrite relationships).

Review follow-ups folded in: the shared driver's IsPeriodic option (a silent no-op since the #1373 neighbor refactor dropped #1291's wrap logic) is restored with per-sibling regression pins, and the per-feature 100k-sentinel worklist refill is removed.

Fixes #1690
Fixes #1691

No parameter changes — parametersVersion() stays at 2.

Approving this PR constitutes the second-engineer sign-off for the report; the Oracle section and provenance sidecar flag the two items to double-check (pure-Phi derivation, folded-distance metric).

Test Plan

  • OrientationAnalysis::CAxisSegmentFeaturesFilter 20/20 on in-core build
  • OrientationAnalysis::CAxisSegmentFeaturesFilter 20/20 on out-of-core build
  • EBSDSegmentFeatures 8/8 and ScalarSegmentFeatures 5/5 in both builds (shared-driver regression check, incl. all-cells-masked -87000 and periodic-wrap pins)
  • Full OrientationAnalysis:: + SimplnxCore:: suites 1205/1205 on in-core build after the shared-driver changes
  • Legacy 6.5.171 A/B comparison rerun post-fix (vv/comparisons/CAxisSegmentFeaturesFilter/results/comparison.md)

@imikejackson
imikejackson force-pushed the vv/c_axis_segmentation branch from 217c54c to 47c921d Compare July 24, 2026 02:16
@imikejackson

imikejackson commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Second-Engineer V&V Review

Full independent review of the V&V package: every claim in vv/CAxisSegmentFeaturesFilter.md, the deviation entries, and the provenance sidecar was re-verified against source, git history, the legacy 6.5.171 sources and official binary, plus an independent A/B rerun with our own comparison methodology. Verified results first, actionable items after.

Verified — no action needed

  • Oracle (both flagged items check out). The pure-Phi derivation was re-derived independently: for quat {sin Φ/2, 0, 0, cos Φ/2} the sample-frame c-axis is (0, ±P·sinΦ, cosΦ), so the dot product between two cells is cos(ΔΦ) regardless of the quaternion P convention — the claimed min(|ΔΦ|, 180−|ΔΦ|) folded metric is exact. Numerically confirmed all four fixture partitions incl. the π-fold pair (w(2°,176°)=174° → folds to 6°). Degree→radian conversion at executeImpl confirmed. The folded metric matches legacy bit-for-bit in structure: v6.5.171 uses the same clamp, acos, and w <= tol || (π − w) <= tol bounds.
  • D1 provenance. git show of BUG: Fixes issue when segmenting features with a +1 feature count #1466 confirms it replaced seed = getSeed(gnum, nextSeed) with raw seed = 0; legacy SegmentFeatures::execute() at the v6.5.171 tag gets every seed (including the first) from getSeed(). The restored driver logic is correct for the seed == -1 initial case (clean -87000, no UB; the old raw-0 burst on a 0-dim geometry even hit a % 0).
  • D2 provenance. At the v6.5.171 tag: m_RandomizeFeatureIds(true) hard-coded, no filter parameter, clock-seeded RNG — exactly as documented. (The parameter only appears on the 6.5.172 branch, commit 5042ce45f.) The official 6.5.171 binary printed "Randomizing Feature Ids" unconditionally on my rerun, so partition-level comparison is indeed forced.
  • Tests. 20 CAxis + 6 EBSD + 3 Scalar all pass at branch head on both NX-Com-Qt69-Vtk95-Rel and simplnx-ooc-Rel. The report's test inventory matches the 20 TEST_CASEs one-for-one; SIMPL 6.4/6.5 conversion fixtures exist; segment_features_test_data.tar.gz is retained for the EBSD tests as stated. parametersVersion() correctly stays at 2 (only parameter description strings changed).
  • A/B revalidation. Regenerated the input via make_input.py, re-ran both runners, and compared with an independent partition-canonicalization script: all four fixtures match at the partition level with identical Active tuple counts, and NX ids equal the oracle exactly — the comparison.md table reproduces.
  • 3-D parity (gap closed, see action item below). All four committed A/B fixtures are N×1×1, so the legacy comparison never exercised the y/z stride branches. I ran a supplementary 3×2×2 fixture (one feature spanning x-, y-, and z-strides plus a π-fold hop, voxel 0 masked) through both runners: full partition parity (5+1 features both sides) and NX equal to an independently computed flood-fill oracle.

Action items

Correctness / coverage (should address before merge):

  • Sibling behavior flip not pinned: all-masked input now errors instead of "succeeding". With the old raw seed = 0 driver, a dataset where no voxel can seed (mask all false, or all phase 0) ran one no-op burst and returned success with one phantom, zero-cell feature for EBSDSegmentFeatures and ScalarSegmentFeatures. Post-fix, the initial getSeed() returns −1 and both now fail with -87000 (EBSDSegmentFeatures.cpp:49-52, ScalarSegmentFeatures.cpp:215-218). This is the correct new behavior (CAxis pins it via Execute Error - No Features Found), but the new sibling regression tests only cover the partially-masked case. Add an all-masked -87000 test per sibling, and consider a one-line note in their eventual deviation docs since this is a user-visible change for pipelines feeding an empty ROI. (addressed in bbbb30a — all-masked -87000 tests added for EBSD and Scalar)
  • -87000 message misdiagnoses its only reachable cause. CAxisSegmentFeatures.cpp:101 says "The C-Axis Misorientation Tolerance may be set too low", but tolerance can never produce zero features — every eligible cell seeds at least a singleton. The error is only reachable when no cell is eligible (all masked / all phase 0 / empty geometry). Suggest wording like "No cell was eligible to seed a Feature (all cells are masked out or have a phase of 0)." Same nit applies to the generic driver-path message in the siblings if you touch them for the item above. (addressed in ffcdb49 — message now names the eligible-seed causes in all three filters)

Hardening (recommended, small):

  • Cancel-at-entry now mutates data before the first cancel check. SegmentFeatures.cpp:153 calls getSeed() (stamps featureIds[seed] = 1; for CAxis also resizes the feature AM) before the loop-top m_ShouldCancel check at line 164. An if(m_ShouldCancel) { return {}; } above line 153 restores the old "no mutation on pre-canceled entry" property. (addressed in ffcdb49)
  • Siblings lack the cancel guard CAxis gained. (addressed in 926f295) On cancel, execute() returns before m_FoundFeatures is assigned, so a direct (non-IFilter) invocation of EBSDSegmentFeatures/ScalarSegmentFeatures misreports -87000 after a cancel. IFilter::execute masks this through the normal API, so low severity — but mirroring the CAxis if(m_ShouldCancel) return {}; guard keeps the three consumers consistent.

Performance:

  • New per-cell mask check in the validation loop is OOC-hostile (the one true perf regression). CAxisSegmentFeatures.cpp:57-79 now calls virtual MaskCompare::isTrue() (bounds-checked DataStore::at()) for every unmasked phase>0 cell, roughly doubling the loop's per-element virtual/chunked accesses on masked datasets — tolerable in-core, potentially minutes at 500M cells on a chunked OOC store. Cheap fix: cache validated phases (std::vector<bool> phaseValidated(numEnsembles)) and continue on already-validated phases before touching the mask; optionally break once all ensembles are validated. Reduces the common all-hex case from O(N) mask reads to O(numEnsembles). (addressed in ffcdb49 — validated-phase cache; mask consulted only for the first cell of each phase)
  • Per-seed resizeTuples in CAxis getSeed is vestigial and quadratic — consider dropping while the lines are hot. (addressed in 926f295getSeed now stamps only, matching EBSD/Scalar; redundant pre-execute Active fill also removed; V&V code-path 13 updated) CAxisSegmentFeatures.cpp:152-158 resizes the feature AM to {gnum+1} on every seed; DataStore::resizeTuples reallocates and copies with no geometric growth, so the Active array pays O(F²) copy traffic plus an AM child-traversal per feature. The siblings (EBSDSegmentFeatures, ScalarSegmentFeatures) only stamp FeatureIds in getSeed and resize once after segmentation — and CAxis operator() already does the authoritative resize + Active refill after execute() returns, so the per-seed resize is dead weight (a legacy-port vestige of updateFeatureInstancePointers()). Pre-existing, but this PR renamed the surrounding lines, and removal aligns CAxis with its siblings.

Docs (trivial):

  • Stale sibling test counts in the report. vv/CAxisSegmentFeaturesFilter.md (Test inventory closing paragraph) says "EBSDSegmentFeatures (5/5) and ScalarSegmentFeatures (2/2)" — the suites are 6/6 and 3/3 since the masked-voxel-0 pins landed. The PR description already has the right numbers. (addressed in 00aadce — now 7/7 and 4/4)
  • Archive a 3-D fixture in the A/B set. Since the committed comparison only proves 1-D parity, add a TC5-style 3×2×2 masked fixture (features spanning y/z strides) to make_input.py + both pipelines + comparison.md so the archived methodology covers the stride branches. I verified parity for exactly this shape; happy to hand over the fixture values. (addressed in 00aadce — TC5_3D added to make_input.py + both pipelines; full 5-fixture suite rerun 2026-07-24, all partitions match)

Follow-up issues (out of scope for this PR):

Adversarial checks that came back clean

Empty-geometry/0-tuple path, nextSeed monotonic-skip invariant, mask-null vs UseMask consistency between the validation loop and getSeed, the −8364 bounds check ordering (cast only after phase > 0), the −651/−652/−653 preflight triangle (a Phases-vs-geometry mismatch cannot slip past preflight), preflight/execute FeatureIds path agreement via getCellDataPath(), quats int64 indexing, voxelsList write-before-grow bounds (safe, no UB), and a subclass sweep (only the three in-repo consumers of SegmentFeatures; all satisfy the stamp-seed-before-return contract the restored first-seed call relies on).

Summary:
- Found and fixed 3 bugs (D1 unvalidated first seed in shared
  SegmentFeatures driver, D4 spurious -8363 rejection of unindexed/
  masked cells + missing CrystalStructures bounds check, D5 stale
  ImageGeom cast crashing RectGrid input);
- documented 5 deviations from DREAM3D 6.5.171 (D1 phantom first
  feature, D2 legacy always clock-randomizes FeatureIds, D3 NX-only
  hexagonal validation, D4 unindexed-cell rejection, D5 RectGrid);
- retired 4 tests (circular-oracle segment_features_test_data
  exemplar consumers; archive remains for EBSDSegmentFeatures);
- unit tests replaced with 13 inlined *Class 1 (Analytical) +
  Class 4 (Invariant)* test fixtures;
- added 3 V&V source-tree deliverables (report, deviations,
  provenance) plus the 6.5.171 A/B comparison pipelines/results;
- documented hexagonal-only requirement, mask/phase-0 semantics,
  RectGrid support, and deterministic FeatureId randomization in
  the user docs.

Signed-off-by: Michael Jackson <mike.jackson@bluequartz.net>
* Add 3x2x2 axis-asymmetric Class 1 fixture pinning the x-fastest voxel
  linearization and the y/z stride neighbor branches (prior fixtures were
  invariant under dims permutation)
* Assert the randomized FeatureIds differ from the canonical labeling so a
  dead randomizer fails the Class 4 test; add a masked variant pinning the
  FeatureId 0 -> 0 mapping through the shuffle
* Preflight now rejects cell arrays whose tuple count differs from the
  geometry cell count (-652) instead of walking out of bounds at execute
* Create FeatureIds with the cell AttributeMatrix tuple shape rather than
  the Quats array shape
* Return cleanly from a canceled run instead of misreporting -87000
* Update V&V report (18 tests, new paths/pins)

Signed-off-by: Michael Jackson <mike.jackson@bluequartz.net>
* Add -653 preflight guard: the geometry cell AttributeMatrix hosting the
  created FeatureIds must match the geometry cell count (closes the residual
  hole behind the -652 check)
* Add Class 1 fixture with Quats in a sibling AttributeMatrix, pinning the
  preflight/execute FeatureIds path agreement
* Refresh V&V report bookkeeping (20 tests, 10 oracle fixtures, dual-build
  20/20 rerun), name the path-derivation half of the executeImpl fix
* Fix deviation-ID typo in comparison.md; make make_input.py's writer-module
  path overridable via LEGACY_DREAM3D_SKILL_DIR

Signed-off-by: Michael Jackson <mike.jackson@bluequartz.net>
* Remove dead pCrystalStructuresArrayPathValue in preflightImpl (the
  selection parameter already validates existence)
* Rename determineGrouping parameters to camelBack per convention;
  rename getSeed's misnamed randPoint (linear scan) to candidatePoint
* Fix comment typos ("in and EBSD file", "visual clarify") and note the
  hex-validation comment's actual guard wording
* Normalize trailing period on the -87000 message
* Refresh algorithm line count in the V&V report

Signed-off-by: Michael Jackson <mike.jackson@bluequartz.net>
…tests

* Add masked-voxel-0 regression tests to EBSDSegmentFeaturesFilter and
  ScalarSegmentFeatures suites, mirroring the CAxis pin: the first seed
  must come from getSeed(), or a masked index 0 yields a phantom empty
  feature and shifted FeatureIds
* The driver fix lives in src/simplnx/Utilities/SegmentFeatures.cpp; each
  consumer now carries its own pin so a test refactor in one plugin cannot
  silently unpin the core-library behavior

Signed-off-by: Michael Jackson <mike.jackson@bluequartz.net>
* vv_policy.md restricted "algorithmic choice" to Rewrite relationships,
  but Port/Minor-changes reports across the fleet already use it for
  deliberate guards and behavior differences (added validation, changed
  defaults, exposed randomization)
* Reword the taxonomy entry to allow any Algorithm Relationship, keeping
  the Rewrite UUID-defense requirement

Signed-off-by: Michael Jackson <mike.jackson@bluequartz.net>
* Correct the -8363 message to say "every phase that participates in the
  segmentation" now that phase-0/masked cells are exempt; quote the path
  and punctuate the -8362 message; -87000 names the actual tolerance
  parameter; un-shout "zero" in -655
* Add direct <algorithm> include for std::clamp; Ref-suffix the DataStore
  references in getSeed and unify their spelled type; drop redundant
  this-> prefixes; unify cellFeatureAM naming
* Alphabetize NumberParameter.hpp into the Parameters include block;
  rename preflight locals (cellFeatureAMPath, useMask) to match the
  values they hold; normalize parameter-description punctuation
* Tag oracle tests [Class1]/[Class4] per sibling convention; Title Case
  the two new preflight test names (report references updated); collapse
  the three hand-built preflight arg blocks into BuildManualPreflightArgs;
  comment the RectGrid fixture's deliberate geometry-name reuse
* Docs: c-axis is the [0001] direction; point "migration deviation notes"
  at the vv/deviations file; algorithm header docstring describes the
  algorithm rather than the filter
* Provenance "Used by tests" no longer overclaims the preflight tests;
  annotate make_input.py enum constants; drop narration comment and
  unify int64 spelling in SegmentFeatures.cpp

Signed-off-by: Michael Jackson <mike.jackson@bluequartz.net>
* CAxisSegmentFeatures::getSeed now only stamps the seed's FeatureId,
  mirroring the sibling EBSD/Scalar segmentation algorithms; the
  feature AttributeMatrix is resized exactly once after segmentation
  completes. The per-seed resizeAttributeArrays() was a vestige of the
  legacy 6.5 port (needed there to refresh raw Active pointers) and
  cost O(F^2) copy traffic in the number of features.
* Drop the redundant pre-execute Active fill; the authoritative fill
  happens after the post-run resize.
* Mirror the CAxis cancel guard and execute() result propagation into
  EBSDSegmentFeatures and ScalarSegmentFeatures so a canceled run is
  not misreported as -87000 "no Features were detected".
* Update V&V report code-path 13 to match the lean getSeed.

Signed-off-by: Michael Jackson <mike.jackson@bluequartz.net>
…tion

* SegmentFeatures::execute() returns before the first getSeed() when the
  run is already canceled on entry, so a pre-canceled run mutates nothing
  (getSeed stamps the seed cell's FeatureId as a side effect).
* Cache validated phases in the CAxis crystal-structure loop so the mask
  is consulted only for the first cell of each phase instead of every
  cell; per-element mask access is expensive on out-of-core stores.
* Reword the -87000 error in all three segmentation filters: the only
  reachable cause is that no cell was eligible to seed (masked out or
  unindexed), not a tolerance/threshold setting.

Signed-off-by: Michael Jackson <mike.jackson@bluequartz.net>
* With the validated first seed, a dataset where no voxel can seed now
  fails with -87000 instead of "succeeding" with one phantom, zero-cell
  feature; pin that behavior per sibling so the shared-driver change is
  covered beyond the partially-masked case.

Signed-off-by: Michael Jackson <mike.jackson@bluequartz.net>
* TC5_3D (3x2x2, bool mask excluding voxels 0 and 5) exercises the y/z
  stride branches of the 3-D flood fill against 6.5.171; the original
  four fixtures are all 1-D lines. One feature spans x/y/z hops plus a
  pi-fold link; masked voxel 0 pins the validated-first-seed path in 3-D.
* Full five-fixture suite rerun through both runners 2026-07-24: all
  partitions match with identical feature counts; NX equals the oracle.
* Refresh V&V report: five-fixture comparison, current sibling suite
  counts (EBSD 7/7, Scalar 4/4).
* nx.d3dpipeline writer args updated to parameters_version 2
  (use_compression/compression_level) to silence the -5432 load warning.

Signed-off-by: Michael Jackson <mike.jackson@bluequartz.net>
… refills

* The IsPeriodic option (added in BlueQuartzSoftware#1291) has been silently ignored since
  the BlueQuartzSoftware#1373 neighbor refactor dropped the wrap logic from the shared
  SegmentFeatures driver. getFaceNeighbors/getAllNeighbors now take
  isPeriodic and emit the wrap-around neighbor for boundary cells (each
  axis wraps independently; axes of extent 1 never wrap), and the driver
  reports when a Feature crosses a periodic boundary, matching the
  documented behavior. Pinned by periodic-wrap tests for EBSD (face) and
  Scalar (face and 26-neighbor schemes).
* The burst worklist was shrunk to size 1 after every feature, so the
  first accepted neighbor of each subsequent feature re-grew it by 100k
  elements and re-wrote 100k -1 sentinels that nothing ever reads. The
  list now keeps its high-water size across features and is never
  sentinel-filled; only indices below the live count are read.

Fixes BlueQuartzSoftware#1690
Fixes BlueQuartzSoftware#1691

Signed-off-by: Michael Jackson <mike.jackson@bluequartz.net>
@imikejackson
imikejackson force-pushed the vv/c_axis_segmentation branch from 88b4898 to 20bcb38 Compare July 27, 2026 18:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG: IsPeriodic parameter is ignored by the SegmentFeatures driver PERF: SegmentFeatures driver re-initializes the voxel worklist for every feature

2 participants