ENH: PoleFigure, IPF Color and many other enhancements#46
Open
imikejackson wants to merge 32 commits into
Open
ENH: PoleFigure, IPF Color and many other enhancements#46imikejackson wants to merge 32 commits into
imikejackson wants to merge 32 commits into
Conversation
Fix LaueOps::_calcRodNearestOrigin for 180° rotations (quaternion-space
reduction avoids tan(theta/2)=inf singularity). Adopt MTEX X||a* convention
for hex and trigonal crystal classes. Expand prismatic pole-figure direction
enumeration for all low-symmetry Laue classes (3, 4, 6, 23, 32) so each PF
now shows the full crystal-symmetry orbit of poles.
Key changes:
- LaueOps.cpp: _calcRodNearestOrigin rewritten in quaternion space
- HexagonalOps.cpp: swap {10-10} and {2-1-10} direction triplets to X||a*
- HexagonalLowOps.cpp: rename PFs to <0001>/<10-10>/<11-20>; enumerate
6-fold orbit (k_SymSize={2,6,6})
- TrigonalOps.cpp: enumerate 3-fold orbit for <0-110> and <1-100>
(k_SymSize={2,6,6})
- TrigonalLowOps.cpp: enumerate 3-fold orbit (k_SymSize={2,6,6})
- TetragonalLowOps.cpp: rename <010> to <110>; enumerate 4-fold orbit
(k_SymSize={2,4,4})
- CubicLowOps.cpp: fix typo in 4th {011} direction (was antipodal dup)
- ComputeStereographicProjection.cpp: restore original axis mapping so
flipFinalImage=true (default) gives MTEX layout
- ODFTest.cpp: drop flipFinalImage=false override; export sampled Eulers
to CSV for MTEX comparison
New tests and tools:
- OrientationTransformationTest.cpp: regression test for 180° FZ reduction
- PoleFigureLaueComparisonTest.cpp: generates per-Laue-class eulers.csv +
composite pole figure TIFF for side-by-side validation against MTEX
- Code_Review/compare_pole_figures_all_laue.m: MATLAB MTEX comparison
script that emits mtex.png per class
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bring the IColorKey refactor and new color key implementations from topic/color_palettes (28c42ef and ancestors) onto the pole-figure convention work on this branch. Contents: - IColorKey abstract interface for pluggable IPF color strategies - TSLColorKey (extracted from LaueOps::computeIPFColor) - NolzeHielscherColorKey implementing the MTEX Nolze-Hielscher algorithm - FundamentalSectorGeometry with polar coordinates for all 11 Laue groups - GriddedColorKey decorator for MTEX-style legend rendering - generate_ipf_legends app outputs both TSL and Nolze-Hielscher legends - MTEX reference TIFFs under Data/IPF_Legend/MTEX_Reference - New tests: ColorSpaceUtilsTest, FundamentalSectorGeometryTest, TSLColorKeyTest, NolzeHielscherColorKeyTest, GriddedColorKeyTest, expanded IPFLegendTest All 34 new color-key tests pass; the one remaining failure is the pre-existing PoleFigureCompositorTest::All_Laue_Classes exemplar mismatch that will be addressed with a fresh exemplar regeneration. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bring the Inverse Pole Figure implementation from topic/add_inverse_pole_figure (7db08ee and ancestors) onto the combined pole-figure / color-palette branch. Contents: - InversePoleFigureUtilities: compute IPF intensity images with SST stereographic mapping, annotation pipeline (labels + color bar) - Shared IPF annotation pipeline declarations in LaueOps - All 11 LaueOps subclasses refactored to use the shared pipeline - generate_pole_figure and make_ipf apps now support both .ang and .ctf formats - Source/Test/InversePoleFigureTest.cpp with regression coverage Conflicts resolved: - LaueOps.cpp: merge canvas_ity.hpp + Fonts.hpp includes from IPF with GriddedColorKey + TSLColorKey includes from color_palettes - LaueOps.h: keep both IColorKey/GriddedColorKey and InversePoleFigureUtilities includes - Source/Test/CMakeLists.txt: keep both PoleFigureLaueComparisonTest.cpp and InversePoleFigureTest.cpp All 345 tests pass except the pre-existing PoleFigureCompositorTest::All_Laue_Classes exemplar mismatch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add two new test cases to IPFLegendTest.cpp:
- IPFLegendTest::CAxisIsRed — for each of the 11 unique Laue classes,
asserts that the TSL IPF color at the crystal c-axis direction
(sample refDir pointing along crystal [001]/[0001]) is pure red.
This is a convention sanity check: chi=0 must map to R=1,G=0,B=0
in every class. Catches Euler->matrix sign flips, symmetry bugs
that relocate the triangle vertex, and TSL color-key regressions.
33 assertions, all pass.
- IPFLegendTest::MTEXCompare_AllLaueClasses — per-class dump of the
EbsdLib TSL IPF legend to
Testing/Temporary/IPFComparison/<rpg>/ebsdlib.tiff
plus a manifest.txt index. The companion MATLAB script at
Code_Review/compare_ipf_legends_all_laue.m uses MTEX's ipfHSVKey
to write mtex.png to the same directories for visual side-by-side
comparison, analogous to the PoleFigureLaueComparisonTest pipeline.
All 346 previously-passing tests still pass; only the pre-existing
PoleFigureCompositorTest::All_Laue_Classes exemplar mismatch remains.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EbsdLib's TSLColorKey and MTEX's ipfHSVKey are different IPF color schemes — comparing them was apples-to-oranges. Switch the IPFLegendTest::MTEXCompare_AllLaueClasses test to emit BOTH the TSL legend (compare against MTEX ipfTSLKey) and the Nolze-Hielscher legend (compare against MTEX ipfHSVKey, which is the HSV scheme NH is modeled on). - IPFLegendTest.cpp: add SectorForRotationPointGroup helper that maps each rotation point group to the matching FundamentalSectorGeometry factory, so per-class NolzeHielscherColorKey instances use the correct sector instead of the cubicHigh placeholder. The test now writes ebsdlib_ipf_legend_tsl.tiff and ebsdlib_ipf_legend_nh.tiff to each Laue class subdirectory; the active color key is restored to TSL after the NH render so following tests see the default. - compare_ipf_legends_all_laue.m: emit mtex_ipf_legend_tsl.png via ipfTSLKey (or ipfHKLKey fallback) and mtex_ipf_legend_hsv.png via ipfHSVKey, so the two pairs can be inspected side-by-side per Laue class. 44 assertions in the comparison test pass (4 per class); full suite still 346/347 with only the pre-existing exemplar mismatch remaining. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MTEX renders IPF color keys by sampling at coarse (1-degree) regular grid points and flat-shading the resulting cells, rather than computing exact per-pixel colors. EbsdLib's GriddedColorKey is a decorator that wraps any IColorKey and reproduces that approach: it precomputes a grid at the requested resolution and snaps each pixel lookup to its nearest grid cell. Wire that decorator into IPFLegendTest::MTEXCompare_AllLaueClasses so each Laue class subdirectory now contains three EbsdLib variants: tsl_ebsdlib_ipf_legend.tiff per-pixel TSL nh_ebsdlib_ipf_legend.tiff per-pixel Nolze-Hielscher nh_gridded_ebsdlib_ipf_legend.tiff 1-degree-gridded NH (MTEX-style) The third should match MTEX ipfHSVKey output much more closely than the per-pixel NH variant because both are flat-shading the same coarse sample grid. 66 assertions in the comparison test pass (6 per class * 11 classes); full suite still 346/347 with only the pre-existing exemplar mismatch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MTEX renders all its IPF color keys via the same 1-degree gridded flat-shading approach, so for an apples-to-apples comparison against MTEX ipfTSLKey the EbsdLib TSL legend should also go through the GriddedColorKey decorator. Add a tsl_gridded_ebsdlib_ipf_legend.tiff variant alongside the existing per-pixel tsl_ebsdlib_ipf_legend.tiff. Each Laue class subdirectory now contains four EbsdLib variants: tsl_ebsdlib_ipf_legend.tiff per-pixel TSL tsl_gridded_ebsdlib_ipf_legend.tiff 1-degree-gridded TSL <-- new nh_ebsdlib_ipf_legend.tiff per-pixel Nolze-Hielscher nh_gridded_ebsdlib_ipf_legend.tiff 1-degree-gridded NH 88 assertions in the comparison test (8 per class * 11 classes), suite still 346/347. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CreateIPFLegend in HexagonalOps.cpp filters pixels outside the hexagonal-high standard stereographic triangle by checking x < 0 to mask the left half of the unit disk. Stereographic x=0 (the y-axis column) was falling through the strict-less-than check and getting a computed color, leaving a single-pixel vertical line down the middle of the rendered legend. Change x < 0.0F to x <= 0.0F so the y-axis column is treated as outside the SST and rendered white. Affects only HexagonalOps (622). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Source/EbsdLib/Utilities/GriddedColorKey.cpp::direction2Color(eta, chi,
angleLimits) was ignoring its angleLimits parameter and looking up colors
from a precomputed grid. The grid was filled in precomputeGrid() by
calling m_InnerKey->direction2Color(dir) — the Vec3 overload — which for
TSLColorKey uses m_DefaultAngleLimits = {0, π/4, acos(1/√3)} (cubic m-3m
limits, etaMax=45°, chiMax≈35.26°).
Result: every IPF legend rendered through GriddedColorKey for any
non-cubic Laue class was being colored with the cubic TSL formula
stamped onto the wrong-shaped sector. For Hex 6/mmm (etaMax=30°,
chiMax=90°) this produced a tiny red patch at [0001], cyan/green
saturation across the rest of the triangle, and no blue at the
{2-1-10} vertex — the per-pixel TSL version was correct because it
calls the inner key's 3-arg overload directly.
Fix: in the 3-arg overload, snap (eta, chi) to grid cells (preserves
the flat-shaded MTEX-style rendering goal) and call the inner key's
3-arg overload with the snapped coordinates and the caller's
angleLimits. The precomputed grid is left in place for the Vec3
overload path (which is documented to use the inner key's defaults).
Add regression test
GriddedColorKey::HonorsAngleLimitsIn3ArgOverload that asserts the
3-arg gridded color matches the per-pixel TSL color under hex limits
and is distinct from the cubic-limit color (so the test would have
caught the bug). Test was RED before the fix (gridded returned
(NaN, 0.92, 0.65) — sqrt(negative) from cubic formula evaluated at
chi > cubic chiMax), and is GREEN after.
Visual verification: 622 tsl_gridded_ebsdlib_ipf_legend.tiff is now
indistinguishable from tsl_ebsdlib_ipf_legend.tiff. Full suite still
347/348 with the same pre-existing exemplar failure.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two follow-up fixes to the GriddedColorKey 3-arg overload uncovered by visual inspection of the per-class IPF legend output: 1. Negative-eta wrap-around (Triclinic, Trigonal-3, -3m). The previous version added 2π to negative eta before snapping, a leftover from when the precomputed grid was indexed in [0, 2π]. After removing the grid lookup, that wrap is harmful: Trigonal-3 has etaMin=-120° and -3m has etaMin=-90°, and the inner TSL formula uses |eta - etaMin| which already handles negative eta correctly. Adding 2π to e.g. eta=-60° remaps it to +300°, which is wildly outside the class's etaMin..etaMax range and produces NaN colors. Pass eta through unchanged. 2. Boundary snap clamp (Cubic m-3m). For cubic high, chiMax is a function of eta (the curved [011]-[111] edge of the standard triangle). The legend renderer passes angleLimits[2] = chiMax(original_eta) to direction2Color, but the snap shifts eta to a different grid cell — and snappedChi can end up slightly larger than the chiMax that was passed in. The TSL formula r = 1 - chi/chiMax then goes negative, sqrt produces NaN, the cast to int produces 0 for the red channel only, and the result is a stippled dark/gray dashed line along the curved edge of the legend. Clamp snappedEta to [angleLimits[0], angleLimits[1]] and snappedChi to [0, angleLimits[2]] before handing off to the inner key. Two new regression tests covering both cases: - HandlesNegativeEta: -3m angleLimits, eta=-60° - BoundarySnapDoesNotProduceNaN: cubic m-3m near the curved edge Visual verification: 1/3/32 tsl_gridded legends now match per-pixel, 432 right-edge stipple is gone. Full suite still 349/350 with only the pre-existing exemplar failure. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous commit clamped both snappedEta and snappedChi to the caller's angleLimits to suppress the cubic m-3m boundary stipple. The eta clamp was overcautious and broke Triclinic (-1) coloring. Triclinic has angleLimits = (etaMin=0, etaMax=π, chiMax=π/2) — etaMax is 180°, and the legend renders the full stereographic disk. Pixels in the lower half of the disk get eta from atan2 in [-π, 0]; the TSL formula relies on |eta - etaMin| = |eta| to produce mirror-symmetric coloring about the y=0 axis. Clamping snappedEta to [0, π] sent every lower-hemisphere pixel to eta=0, collapsing the bottom half of the Triclinic disk to a single eta value. Drop the eta clamp. Only the chi clamp is required for the cubic boundary fix (chi is always in [0, π/2] after FZ folding for every Laue class, so clamping it to [0, angleLimits[2]] is safe and only affects boundary pixels of cubic m-3m). New regression test GriddedColorKey::TriclinicNegativeEtaProducesColor pins the behavior so this can't happen again. 8 GriddedColorKey test cases (1662 assertions) pass; 11/11 Laue classes render correctly per visual inspection. Suite still 349/350 with only the pre-existing exemplar failure. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add Source/EbsdLib/Utilities/PngWriter.{h,cpp} with the same API as
TiffWriter, backed by stb_image_write. Pull stb in via vcpkg (already
declared in vcpkg.json). Replace every TIFF output call site in apps
and tests with PngWriter, and rename output filenames from .tiff/.tif
to .png:
- Source/Apps/{make_ipf, generate_pole_figure, generate_ipf_legends,
generate_ipf_from_file, generate_ipf_density}.cpp
- Source/Test/{IPFLegendTest, ODFTest,
PoleFigureLaueComparisonTest, PoleFigureCompositorTest}.cpp
- Source/Apps/mtex_generate_legends.m
- Code_Review/{mtex_ang_to_ipf, compare_pole_figures_all_laue,
compare_ipf_legends_all_laue}.m
- Code_Review/coloring_schemes_vs_mtex.md (output-path examples)
PNG aligns the outputs with what MTEX writes by default, simplifying
pixel-level comparison, and shrinks the per-image file size by ~30x
(uncompressed RGB TIFF -> PNG with deflate). TiffWriter and its test
(TiffWriterTest.cpp) stay untouched for callers that want TIFF, but
the IPF/PF pipeline produces PNG end-to-end now.
Source/Apps/mtex_ang_to_ipf.m output target also moved to .png. New
in-tree dataset Data/ipf_color_tests (AllLaueClasses_RandO.ang plus
EDAX_TSL_IPF.bmp / EDAX_PUCM_IPF.bmp references) tracked, plus the
shell wrapper for the MTEX script.
Suite still 351/352 with the same pre-existing exemplar failure.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add an EDAX TSL validation section to coloring_schemes_vs_mtex.md documenting the result of running AllLaueClasses_RandO.ang through make_ipf and pixel-diffing against EDAX_TSL_IPF.bmp (the TSL-via-OIM-Analysis reference). Headline: EbsdLib's TSL color key matches EDAX's TSL to within sub-pixel accuracy on 11 of 12 Laue classes (mean per-channel diff < 3 / 255). The whole-image mean diff is 3.74 with only 5.8% of pixels differing at all; the remainder is consistent with antialias noise from the 5.33x non-integer BMP upscale. MTEX TSL disagrees with EDAX TSL by an order of magnitude more (mean 30.5) — confirms the documented chi-eta vs polar-HSL divergence. Mono-c is the one outlier (mean=31.82 / max=244 over 46% of pixels). Logged as an open question — likely a b-setting vs c-setting axis convention mismatch worth investigating in isolation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Port William Lenthe's BSD-3 reference implementation of the EDAX "perceptually uniform" IPF color scheme into EbsdLib as a third IColorKey: - Source/EbsdLib/Utilities/wlenthe_orientation_coloring.hpp: vendored verbatim with attribution from https://github.com/wlenthe/crystallography (BSD-3, 2017) Implements the orientation coloring algorithm of Nolze & Hielscher (J. Appl. Crystallogr. 49.5 (2016): 1786-1802) and is the same reference code that EDAX OIM Analysis ships behind their PUCM IPF color palette. - Source/EbsdLib/Utilities/PUCMColorKey.{hpp,cpp}: thin dispatch wrapper that selects the per-Laue-class wlenthe entry point based on the rotation point group string ("1", "2", ..., "432"). Maps the Vec3 and (eta, chi, angleLimits) IColorKey overloads to hemiIpf / cyclicIpf<N> / dihedralIpf<N> / cubicLowIpf / cubicIpf. - Source/Test/PUCMColorKeyTest.cpp: 6 test cases / 140 assertions covering construction validation, name string, c-axis sanity, distinct-FZ-vertex distinct-color, finite-color across all 11 Laue classes, and 3-arg <-> Vec3 overload consistency. - Source/Apps/make_ipf.cpp: optional 3rd argument tsl|pucm picks the color key. Pass the configured ops vector into the GenerateIPFColorsImpl ctor (was creating fresh ops internally with default TSL keys, which discarded any per-class color-key configuration). Validation against EDAX_PUCM_IPF.bmp ( Data/ipf_color_tests/AllLaueClasses_RandO.ang reference output): - whole-image mean per-channel diff = 9.01 / 765, only 26.5% of pixels differ at all - 9 of 12 Laue classes match to mean diff < 3 (essentially pixel-equivalent within antialias noise of the 5.33x BMP upscale) - known outliers: mono c (mean 74.67, same as TSL — phase mapping bug, not PUCM specific), tet 4/m (10.05) and ditet 4/mmm (11.78) with moderate divergence; all logged in Code_Review/coloring_schemes_vs_mtex.md "Open questions". Suite: 357/358 (added 6 PUCM tests, same pre-existing exemplar failure). Co-Authored-By: William C. Lenthe (orientation_coloring.hpp, BSD-3) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Set PUCMColorKey(rpg) on each LaueOps inside IPFLegendTest::NH_Compare_MTEX_IPF_Legends and call generateIPFTriangleLegend twice — once per-pixel, once wrapped in GriddedColorKey for 1-degree flat-shaded cells. Each Laue-class subdir under Testing/Temporary/IPFComparison/ now also contains: pucm_ebsdlib_ipf_legend.png pucm_gridded_ebsdlib_ipf_legend.png alongside the existing TSL and NH legends. Verified visually for hex 6/mmm: full HSV cycle around the FZ perimeter with white-ish center, matching the perceptually-uniform style EDAX PUCM produces. Suite still 357/358 with same pre-existing exemplar failure; NH_Compare test goes from 88 to ~132 assertions (4 extra REQUIREs per Laue class * 11 unique classes). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add the architectural plumbing for hex/trig convention-aware symmetry
operations to HexagonalOps. The canonical k_QuatSym / k_RodSym / k_MatSym
arrays continue to hold the X||a* (MTEX / Oxford / v3 default) form
unchanged. A new SymOps struct + templated build<HexConvention> factory
derives the X||a (TSL / EDAX / legacy DREAM3D) form via similarity transform
S_X||a = q30 * S_X||a* * conj(q30)
where q30 is a 30°-about-c rotation. Two static const SymOps instances
exist at TU static-init -- one per convention -- so future rendering
methods can dispatch on the caller-supplied HexConvention by pointer flip.
This is pure plumbing. No rendering method consults the new tables yet.
Output is bit-identical to current v3:
- EbsdLib library + 12 apps + unit tests: clean compile
- DREAM3D-NX downstream: clean compile (235/235 targets)
- PoleFigurePositionTest::EmitCsv (1752-bucket MTEX validation): pass
- 47/48 Hex/PF/IPF unit tests: pass identically
- 1/48 (PoleFigureCompositorTest::All_Laue_Classes): pre-existing
exemplar failure, identical with this change reverted
Subsequent PRs (PR 2b, 2c) will wire generatePoleFigure /
generateSphereCoordsFromEulers / generateIPFColor to consult the
convention parameter and pick the right SymOps instance. PR 2d will
propagate this same SymOps pattern to HexagonalLowOps, TrigonalOps,
TrigonalLowOps. Hand-flip of canonical to v2 X||a values deferred to
PR 2e once the architecture is exercised end-to-end.
See Code_Review/v3_phase0_design_notes.md §5 for the full design.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… (PR 2b)
Wire the SymOps machinery from PR 2a into the HexagonalOps pole-figure
direction-table dispatch so that callers passing config.hexConvention =
XParallelA via PoleFigureConfiguration_t now get X||a-form sample-frame
projections, while the default XParallelAStar continues to produce the
current v3 X||a* output unchanged.
Changes by file:
LaueOps.h
- generateSphereCoordsFromEulers virtual gains a HexConvention conv
parameter with default XParallelAStar (matches the per-rendering-
method pattern from PR 1).
All 11 *Ops.{h,cpp}
- Override declarations + definitions updated with the new parameter.
- Non-hex/trig classes (cubic, tet, ortho, mono, tri) accept the
parameter but ignore it -- no basal-plane convention exists.
- HexagonalLow / TrigonalHigh / TrigonalLow accept it but still use
their existing X||a* hardcoded direction tables; they will get the
same SymOps refactor in PR 2d.
HexagonalOps.cpp
- SymOps struct extended with three plane-family direction tables
(dirsFamily0/1/2). The XParallelAStar branch holds the canonical
v3 cartesian values; the XParallelA branch derives them by R_z(+30°)
rotation (basal-plane vectors rotate; c-axis is invariant).
- GenerateSphereCoordsImpl now takes a const SymOps* and replaces
its inline hardcoded direction blocks with a small helper function
that emits each direction + antipode pair via a uniform loop. The
antipode-emit semantics are unchanged.
- HexagonalOps::generateSphereCoordsFromEulers picks the right SymOps
instance based on the incoming HexConvention and forwards it to
GenerateSphereCoordsImpl.
- HexagonalOps::generatePoleFigure forwards config.hexConvention to
generateSphereCoordsFromEulers.
Source/Test/LaueOpsTest.cpp
- New test GenerateSphereCoords_HexConvention_HexagonalOps verifies
the {10-10} family-1 first member sits at (1, 0, 0) under default
XParallelAStar and at (cos30°, sin30°, 0) under explicit XParallelA.
- Confirms the c-axis ({0001} family) is convention-invariant.
- Confirms the two outputs differ by more than FP noise (i.e., the
opt-in path actually does something).
Verification:
- EbsdLib: clean compile (51 TUs)
- DREAM3D-NX: clean compile (235 targets)
- 76/77 hex/PF/IPF tests pass; new HexConvention test passes
- 1/77 (PoleFigureCompositorTest::All_Laue_Classes): pre-existing
exemplar-mismatch failure, identical with this change reverted
- PoleFigurePositionTest (1752-bucket MTEX validation): pass
(default path bit-identical)
Hand-derivation of one entry: 180°-about-a* (canonical k_QuatSym[5] =
(1, 0, 0, 0)) under conjugation by q_30 = R_z(+30°) yields
(cos30°, sin30°, 0, 0) ≈ (0.866, 0.5, 0, 0), which is 180° about the
axis at +30° from X under X||a basis -- correct for a* expressed in
X||a (since a* is at +30° from a = X). Conjugation direction confirmed.
PR 2c will wire the IPF color path (computeIPFColor / generateIPFColor /
generateIPFTriangleLegend) to consult the convention. PR 2d propagates
the SymOps pattern to HexagonalLow / TrigonalHigh / TrigonalLow.
See Code_Review/v3_phase0_design_notes.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wire the SymOps machinery into the HexagonalOps IPF color and triangle
legend paths so that callers passing HexConvention::XParallelA exercise
the X||a sym op tables. Default XParallelAStar path stays bit-identical.
Changes:
HexagonalOps.h
- Add private generateIPFColorImpl(eulers, refDir, degToRad, conv)
helper. Both generateIPFColor overloads route through it.
HexagonalOps.cpp
- generateIPFColorImpl mirrors LaueOps::computeIPFColor exactly,
with the FZ-reduction loop reading sym->quat[j] (selected by
conv) instead of the convention-blind getQuatSymOp(j). Other
logic (eta/chi extraction, color key dispatch, fallback color
computation) is unchanged.
- Both generateIPFColor overloads now call the convention-aware
helper instead of the base-class computeIPFColor.
- CreateIPFLegend extended with a HexConvention parameter that
forwards to its inner generateIPFColor call.
- generateIPFTriangleLegend forwards conv to CreateIPFLegend.
Source/Test/LaueOpsTest.cpp
- New test GenerateIPFColor_HexConvention_HexagonalOps.
For HexagonalOps (Laue class 6/mmm), IPF color is genuinely
convention-INVARIANT: the SST is reached via c-axis rotations
plus the inversion fold (p[2] < 0 -> flip). Basal-plane 180°
sym ops differ between X||a and X||a* but are operationally
redundant for centrosymmetric 6/mmm -- any orientation
reachable via a basal op is also reachable via c-rotation +
inversion. So the test asserts the two conventions produce
IDENTICAL non-trivial RGB. PR 2d will exercise hex-low /
trigonal classes where the SST geometry differs and IPF
colors may genuinely depend on the convention.
Test also verifies that explicit XParallelAStar matches
default and that the resulting RGB is non-zero (guards
against silent sym-op-zero garbage).
Verification:
- EbsdLib library + apps + tests: clean compile
- DREAM3D-NX downstream: clean compile (235 targets)
- 76/77 hex/PF/IPF/ColorKey tests pass
- 1/77 PoleFigureCompositorTest::All_Laue_Classes: pre-existing
exemplar failure, unchanged by this PR
- GenerateIPFColor_HexConvention_HexagonalOps (new): pass
- GenerateSphereCoords_HexConvention_HexagonalOps (PR 2b): still pass
- PoleFigurePositionTest (1752-bucket MTEX validation): pass
The PR 2 slicing continues:
PR 2d -- propagate SymOps + convention dispatch to HexagonalLowOps,
TrigonalOps, TrigonalLowOps. The 30°-vs-60° SST difference
for those classes (combined with potentially-different
inversion / mirror availability) is where IPF color
convention-dependence may manifest.
PR 2e -- hand-flip canonical to v2 X||a values once the architecture
is exercised end-to-end.
PR 3 -- remove default values; force simplnx audit.
See Code_Review/v3_phase0_design_notes.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… TrigLow (PR 2d)
Apply the same SymOps + templated build<HexConvention> + per-method
convention dispatch pattern that landed for HexagonalOps in PRs 2a/2b/2c
to the remaining three hex/trig classes:
- HexagonalLowOps (Laue 6/m)
- TrigonalOps (Laue -3m)
- TrigonalLowOps (Laue -3)
For each class:
- Add SymOps struct in the namespace block (after k_QuatSym / k_RodSym /
k_MatSym), with quat/rod/mat tables PLUS three plane-family direction
tables (dirsFamily0/1/2). The XParallelAStar branch trivially copies
the canonical (current v3) tables. The XParallelA branch derives
everything via 30°-about-c similarity transform: sym ops via
quaternion conjugation, direction tables via R_z(+30°) cartesian
rotation. c-axis ({0001}) is invariant.
- Two static const SymOps instances per namespace (k_SymOps_XParallelA*
and k_SymOps_XParallelA), built once at TU static-init.
- GenerateSphereCoordsImpl refactored to take a const SymOps* and emit
each direction + antipode pair via a uniform helper. Inline hardcoded
direction blocks replaced with loops over the convention-selected
direction tables.
- generateSphereCoordsFromEulers picks the SymOps based on `conv` and
forwards to GenerateSphereCoordsImpl.
- generatePoleFigure forwards config.hexConvention to its
generateSphereCoordsFromEulers call.
- Private generateIPFColorImpl helper (declared in the .h, defined in
the .cpp) mirrors LaueOps::computeIPFColor with sym->quat[j] in the
FZ-reduction loop. Both generateIPFColor overloads now route through
this helper.
- CreateIPFLegend extended with a HexConvention parameter that
forwards to its inner generateIPFColor call.
- generateIPFTriangleLegend forwards conv to CreateIPFLegend.
Notes on convention-dependence of IPF color per Laue class:
- HexagonalLow (6/m): all sym ops are c-axis rotations (no basal-plane
180° flips). The conjugation is mathematically a no-op for the sym
ops; only the direction tables are convention-dependent.
- TrigonalHigh (-3m): 3 c-axis rotations + 3 basal-plane 180° flips.
The 180°s are basis-dependent so conjugation yields different sym op
values for those entries.
- TrigonalLow (-3): only c-axis rotations. Same structural property
as HexagonalLow.
Whether IPF color actually differs visibly between the two conventions
for these Laue classes depends on whether the SST representative
selection requires a basal-plane sym op (vs c-axis + inversion fold).
For the centrosymmetric Laue classes shipped with EbsdLib, the inversion
fold (p[2] < 0 -> flip) handles most cases via c-rotations alone, so IPF
color is often convention-invariant in practice. Pole figures, however,
ARE convention-dependent (the family direction tables rotate by 30°).
Source/Test/LaueOpsTest.cpp:
- New tests GenerateSphereCoords_HexConvention_HexagonalLowOps,
_TrigonalOps, _TrigonalLowOps each check that family-1 first member
rotates by R_z(+30°) when caller switches from default XParallelAStar
to explicit XParallelA. c-axis invariance also implicitly verified
via the same shared template helper.
Verification:
- EbsdLib library + apps + tests: clean compile (51 + apps + tests)
- DREAM3D-NX downstream: clean compile (235 targets)
- 80/81 hex/PF/IPF/Trigonal/ColorKey tests pass identically
- 1/81 PoleFigureCompositorTest::All_Laue_Classes: pre-existing
exemplar failure, unchanged by this PR
- All 5 new HexConvention tests pass:
* GenerateIPFColor_HexConvention_HexagonalOps (PR 2c)
* GenerateSphereCoords_HexConvention_HexagonalOps (PR 2b)
* GenerateSphereCoords_HexConvention_HexagonalLowOps (PR 2d, new)
* GenerateSphereCoords_HexConvention_TrigonalOps (PR 2d, new)
* GenerateSphereCoords_HexConvention_TrigonalLowOps (PR 2d, new)
- PoleFigurePositionTest (1752-bucket MTEX validation): pass
Phase 0 architectural plumbing is now complete across all four hex/trig
classes. Pole figures and IPF colors for HexagonalOps, HexagonalLowOps,
TrigonalOps, and TrigonalLowOps now honor the caller-supplied
HexConvention parameter end-to-end. Default XParallelAStar everywhere
preserves current v3 output bit-identically; explicit XParallelA produces
the X||a-form (OIM-Analysis-compatible) rendering.
PR 2e (deferred) -- hand-flip canonical tables to v2 X||a values via
git archaeology so that XParallelA's output is anchored to known v2
correctness rather than computed from X||a* via conjugation. Optional
hardening; the conjugation math is correct by construction.
PR 3 -- remove default values from API; force simplnx audit at every
LaueOps construction / rendering call site.
See Code_Review/v3_phase0_design_notes.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…R 2e)
Phase-0 §5.1 originally proposed canonical=X||a + derived=X||a* via the
templated SymOps factory. PR 2a-2d landed with the opposite direction
(canonical=X||a*, derived=X||a) on the strength of the MTEX-validated
v3 hand-typed tables.
PR 2e investigated whether v3 X||a* values could be hand-flipped to the
v2 X||a values via a uniform R_z(-30) rotation. They cannot: F1 and F2
families chose different orbit members as "first" between v2 and v3.
The user clarified that the EbsdLib hex/trig sym op ordering originates
from the EMsoftOO project, hand-derived for loop efficiency rather than
to encode any mathematical relation between consecutive entries. Two
hand-typed tables encoding the same orbit can therefore legitimately
disagree by index. Validation is by orbit equality, not table equality.
This commit captures that finding without changing source behavior:
* v3_phase0_design_notes.md: new section 16 (canonical-direction
reasoning + v2->v3 enumeration-mismatch finding + lesson for future
Phase-N convention work). Notes added to sections 5.1, 12 (PR 2 /
PR 3 sequencing), 13.2, and 15 (open questions resolved).
* HexagonalOps.cpp / HexagonalLowOps.cpp / TrigonalOps.cpp /
TrigonalLowOps.cpp: SymOps comment block updated to state
"CANONICAL = X||a*" explicitly, mention EMsoftOO ordering, and
point at design-notes section 16.
No code changes; rendering output is bit-identical.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… 2f)
Adds a single CLI tool that ingests a .ang or .ctf file and emits, per
indexed phase, three PNGs honoring the user-supplied HexConvention and
color-key choice:
- composite pole figure (PoleFigureCompositor)
- IPF map (per-pixel generateIPFColor)
- IPF triangle legend (LaueOps::generateIPFTriangleLegend)
Filenames stamp the convention and color-key
(<phase>_phase<N>_{x_a|x_astar}_{tsl|pucm|nh}_{PF|IPF|LEGEND}.png) so a
single user can run all four cells of {convention} x {color-key} and
diff them side by side.
CLI flags:
--convention {x_a, x_astar} (default x_astar)
--color-key {tsl, pucm, nh} (default tsl)
--phase N
--ref-dir x,y,z
--image-dim N --lambert-dim N --legend-dim N
Implementation is split between render_ebsd.cpp (the run() entry point)
and render_ebsd_main.cpp (the CLI parsing + main()), so the same TU can
be linked into the EbsdLibUnitTest binary without colliding with
Catch2's main.
Adds Source/Test/RenderEbsdSmokeTest.cpp (Catch2) covering:
* 4-cell {X||a, X||a*} x {TSL, PUCM} matrix on Phase 4 (Hexagonal_High)
of Data/ipf_color_tests/AllLaueClasses_RandO.ang -- asserts each cell
produces all 3 PNGs with non-trivial size.
* Convention-difference guard: under HexagonalHigh, the composite PF
must differ byte-for-byte between conventions (basal-plane direction
families project differently); the IPF map and legend must match
(6/mmm SST is convention-invariant per LaueOpsTest).
Removes the legacy phi2-30 + 90-deg-z pre-rotation workaround in
make_pole_figure.cpp; the basal-plane convention bridge is now applied
inside LaueOps via config.hexConvention = XParallelA.
Sanity-tested make_pole_figure against AllLaueClasses_RandO.ang: all 12
phase composite PNGs render cleanly. RenderEbsdSmokeTest passes; rest of
the suite is unchanged (the pre-existing PoleFigureCompositorTest::All_Laue_Classes
exemplar-byte mismatch is unaffected by these edits).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PoleFigureCompositor::generatePoleFigures rebuilds a per-call
PoleFigureConfiguration_t pfConfig field-by-field from its incoming
CompositePoleFigureConfiguration_t config -- but it was missing the
hexConvention copy. The downstream call
orientationOps[config.laueOpsIndex]->generatePoleFigure(pfConfig)
therefore always saw pfConfig.hexConvention == XParallelAStar (the
struct default), regardless of what the caller had set on
config.hexConvention. The convention parameter was effectively a
no-op for every pole figure rendered through PoleFigureCompositor.
Reproduced with render_ebsd on /Users/Shared/Data/MTR_Data/RR_MTR_Examples/12.ang
phase 1 (Titanium alpha, hex 6/mmm): X||a and X||a* runs produced
byte-identical composite PFs even though the basal-plane direction
families project differently in the two bases.
Fix: copy config.hexConvention into pfConfig in
PoleFigureCompositor.cpp.
Also tightens render_ebsd: stops baking the convention token into the
composite title. Title text is rasterized into the PNG, which means a
title difference produced byte-different output even when the actual
disk content was identical -- exactly what was happening here. The
filename still stamps the convention; the title now reads just
"<phase> (<sym>)". This makes the smoke test's PF byte-difference
assertion strictly a test of disk content, not text rendering, so a
future convention drop won't slip past as a false positive.
Verified end-to-end:
* Smoke test: 4-cell matrix passes; PF differs / IPF same / LEGEND
same on HexagonalHigh under TSL (consistent with 6/mmm SST
invariance documented in LaueOpsTest).
* 12.ang manual replay (TSL): PF DIFFERS, LEGEND identical.
* 12.ang manual replay (PUCM): PF DIFFERS, LEGEND has a small
deterministic byte-difference (PUCM-specific quirk, unrelated to
this fix).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The four hex/trig drawIPFAnnotations overrides carried hardcoded label
tables under the X||a basis (cartesian +X = a-vector = [2-1-10]) and
ignored the convention parameter. Compounding that, both
LaueOps::annotateIPFImage and the virtual LaueOps::drawIPFAnnotations
took no HexConvention argument, so even after PR 2g wired conv into
the SST coloring path it could not reach the label rendering. Result:
IPF legends rendered identically under X||a and X||a*, even though
the +X corner labels SHOULD be [2-1-10] under X||a and [10-10] under
X||a*.
Plumbing changes:
* Add HexConvention conv parameter (default XParallelAStar) to:
- LaueOps::annotateIPFImage (LaueOps.h, .cpp)
- virtual LaueOps::drawIPFAnnotations (LaueOps.h)
- all 11 drawIPFAnnotations overrides (CubicOps, CubicLowOps,
HexagonalOps, HexagonalLowOps, MonoclinicOps, OrthoRhombicOps,
TetragonalOps, TetragonalLowOps, TriclinicOps, TrigonalOps,
TrigonalLowOps).
* Update annotateIPFImage to forward conv into drawIPFAnnotations.
* Update each hex/trig generateIPFTriangleLegend to forward its
incoming conv argument into annotateIPFImage.
* The non-hex/trig overrides accept conv but ignore it (no basal-
plane convention exists for cubic/tet/ortho/mono/tri).
* The 3-pole-figure path (LaueOps::generateAnnotatedIPFDensity)
continues to use the default convention; threading it requires
adding a hexConvention field to InversePoleFigureConfiguration_t,
deferred to a follow-up PR.
Label tables in the four hex/trig overrides:
* X||a: cartesian +X = a = [2-1-10], +30° = a* = [10-10] (existing
hardcoded values — these stay).
* X||a*: cartesian +X = a* = [10-10]. Equivalent to rotating the
X||a label list one slot to the left:
labels_X_astar[i] = labels_X_a[(i + 1) % 12]
so under X||a* angle 0° = [10-10] and angle 330° = [2-1-10].
Tests:
* LaueOpsTest::GenerateIPFTriangleLegend_HexConvention_HexagonalOps:
renders the SST legend twice (X||a, X||a*) and asserts the two
UInt8ArrayType payloads differ. Locks in the plumbing against a
future regression.
* RenderEbsdSmokeTest::ConventionsDifferOnHexagonalHigh: flipped
the legend assertion from EQUAL to DIFFER. The old comment block
reasoned that the legend was convention-invariant for 6/mmm — that
was true only because the labels were hardcoded; with PR 2h the
Miller-index strings change and the bytes diverge.
End-to-end verified on /Users/Shared/Data/MTR_Data/RR_MTR_Examples/12.ang
phase 1 (Titanium alpha): X||a vs X||a* TSL legend PNGs now differ.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The four hex/trig getDefaultPoleFigureNames overrides each took a HexConvention parameter but ignored it. Compounding that, all four generatePoleFigure methods called getDefaultPoleFigureNames() with NO argument, so even after fixing BlueQuartzSoftware#1 the labels would still ignore the user's choice. Net effect: the third PF panel for HexagonalHigh always read "<2-1-10>" regardless of convention, even though MTEX labels the same family as "<11-20>". The two strings are sym-equivalent under the 6-fold about c -- they describe the same physical {2-1-10} a-family, just with different "first orbit member" choices. OIM/EDAX/legacy DREAM3D pick "<2-1-10>" (the a-vector itself); MTEX picks "<11-20>". Since HexConvention X||a is paired with the OIM toolchain and X||a* with MTEX (per design notes §2), the natural mapping is: Slot X||a X||a* ---- ---- ----- 0 "<0001>" "<0001>" 1 "<10-10>" "<10-10>" 2 "<2-1-10>" "<11-20>" PR 2i changes: * HexagonalOps and HexagonalLowOps: getDefaultPoleFigureNames now branches on conv. Was emitting "<2-1-10>" (HexHigh) and "<11-20>" (HexLow) unconditionally -- inconsistent between the two classes AND inconsistent with the conv argument it was given. Now both classes follow the table above. * TrigonalOps and TrigonalLowOps: -3m has two distinct prism families and the OIM/MTEX label-tradition split for the {2-1-10} family doesn't apply cleanly. Their conv parameter is plumbed but the returned strings are the same under both conventions for now (with a `(void)conv` to mark intent and a comment pointing at this PR's reasoning). * All four generatePoleFigure overrides now call getDefaultPoleFigureNames(config.hexConvention) instead of the no-arg form. This was the actual silent-drop bug. Validation script update: * Data/Pole_Figure_Validation/mtex_pole_figure_positions.m: the hardcoded labels for 6/mmm now read "<11-20>" (matching the library output under the X||a* default). * Data/Pole_Figure_Validation/mtex_pole_figure_positions.csv: regenerated for the 6/mmm a-family rows (72 rows; sed-replaced "<2-1-10>" -> "<11-20>" only on the 6/mmm rows). Other rows (cubic, hex/m, trig, etc.) untouched. PoleFigurePositionTest rebuckets cleanly with the new labels. * mtex_ang_to_pole_figures.m: 6/mmm Miller index for slot 2 updated from {2 -1 -1 0} to {1 1 -2 0} to match. Tests: * LaueOpsTest::GetDefaultPoleFigureNames_HexConvention: asserts Hex high and Hex low return different a-family strings under X||a vs X||a*, that the prism slot is identical, and that trigonal classes produce three non-empty strings under both conventions (no crash, valid). * LaueOpsTest::GeneratePoleFigure_PropagatesHexConvention: asserts HexagonalOps::generatePoleFigure(config) actually threads config.hexConvention through to getDefaultPoleFigureNames. The rendered figure[2] array name carries the label, which we read back via getName(). Catches the silent-drop bug we just fixed. Design notes: * §5.4 rewritten. The previous draft proposed swapping slot ORDER between conventions (e.g. X||a -> {<0001>, <2-1-10>, <10-10>}), which would have misaligned labels with the renderer's fixed family order. PR 2i only swaps the STRING in slot 2; slot order is unchanged. Section now reflects that and notes the trigonal deferral. Verified end-to-end: render_ebsd on 12.ang phase 1 (Titanium alpha) now shows "<11-20>" under X||a* and "<2-1-10>" under X||a, matching what the user observed in MTEX vs OIM Analysis side-by-side. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously, invoking
render_ebsd /path/to/input.ang --convention x_a ...
silently consumed "--convention" as the <output_dir> positional, then
tried to parse "x_a" as a flag and failed with the misleading
"Unknown argument 'x_a'". User had to reverse-engineer the parser to
realize the actual problem was the missing output directory.
Hardenings:
* Detect <output_dir> argument starting with "--" and emit a
targeted error pointing at the missing positional.
* Symmetrically detect <input> argument starting with "--".
* Allow `render_ebsd --help` and `-h` with no other args (previously
--help was only honored as a non-positional flag, so it required
two dummy positional arguments first).
* argc<3 path now prints an explicit "missing positional arguments"
line in addition to usage.
All four invalid-invocation paths now exit with code 1 (preserved);
--help exits with 0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two threads of work bundled here, both accumulated during recent
sessions:
* clang-format reflow across LaueOps subclasses (Cubic*, Tetragonal*,
Monoclinic, Orthorhombic, Triclinic — both .h declarations and .cpp
definitions for the long generateSphereCoordsFromEulers /
generateIPFColor / drawIPFAnnotations signatures), Source/Test/
files, the PUCMColorKey trailing-comment alignment, and the big
wlenthe_orientation_coloring.hpp third-party header (full
reformat to project style: header-order, brace placement, function
bodies on their own lines, sections demarcated by namespace
blocks).
* ODF bin clarifications: every k_OdfNumBins comment now reads
"Represents a 5Deg bin in homochoric space" instead of just
"5Deg bin" -- avoids the implicit conflation with Bunge Euler
grids that MTEX uses (issue raised during PR 2h discussion).
Also adds LaueOps::getOdfBinStepSize() returning {5, 5, 5} so
consumers can pull the constant programmatically rather than
hard-coding it at every call site.
Source/Apps/render_ebsd.h: minor formatting (field comment
alignment).
generate_ipf_legends.cpp / generate_pole_figure.cpp: trivial single-line
adjustments from the format pass.
Behavioral change: none beyond the new getOdfBinStepSize() helper,
which has no callers in the library yet -- it just exposes a constant
that was previously implicit. All 369 currently-passing tests continue
to pass; the one pre-existing failure
(PoleFigureCompositorTest::All_Laue_Classes, a known canvas_ity
FP-determinism issue) is unaffected.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The IPF triangle legend renderer paints a small SST wedge onto a
canvasDim x canvasDim canvas. For hex/trig classes whose SST is a
narrow wedge of the unit circle (HexHigh's is a 30° slice) this
leaves the rendered PNG dominated by white margin on every side.
MTEX's legend export is sized to the painted region; ours wasn't.
PR 2j adds a post-render auto-crop step:
* New utility ebsdlib::CropImageToContent(src, w, h, channels, padding)
in Source/EbsdLib/Utilities/ImageCrop.{hpp,cpp}. Finds the
bounding box of non-white pixels (background = (255, 255, 255)
exact match), applies symmetric padding, clamps to canvas.
Returns a struct {image, width, height} so callers carry the
new dims with the buffer.
* render_ebsd::writeLegend now calls this helper after generating
the legend, then writes the PNG with the cropped dims. Default
padding 12 px around the painted region.
* Existing LaueOps::generateIPFTriangleLegend behaviour is
UNCHANGED -- it still returns canvasDim x canvasDim. Crop is
purely a render_ebsd concern; other consumers keep their fixed
output guarantee.
Tests (Source/Test/ImageCropTest.cpp, 5 cases):
* CropsToBoundingBoxPlusPadding -- synthetic 20x10 rect on a 100x100
canvas with padding=4 produces a 28x18 output.
* ClampsBoundingBoxToCanvas -- corner-rect with padding=100 clamps
cleanly to the original canvas.
* AllWhiteImageReturnsOriginalSize -- pathological all-white input
is returned unchanged (no infinite-bounding-box).
* CroppedPixelsMatchSource -- center pixel of cropped output equals
the corresponding source pixel (no rotation/mirror artifacts).
* HexagonalHighLegendIsCroppedSmaller -- end-to-end: render a
real HexagonalOps legend at 512x512, crop, assert the result is
smaller than 512x512 (proves crop fires) and larger than 128x128
(proves we didn't trim actual content).
End-to-end on /Users/Shared/Data/MTR_Data/RR_MTR_Examples/12.ang
phase 1 (Titanium alpha, Hex 6/mmm), --legend-dim 1024:
Before PR 2j: 1024 x 1024 PNG
After PR 2j: 732 x 413 PNG (aspect ratio matches the 30° SST wedge)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sets up the structural plumbing needed to validate the IPF density
path against MTEX. The colored SST raster is convention-invariant for
hex/trig (the eta-chi region of the FZ doesn't move between bases),
but the Miller-index labels drawn around each SST in the annotated
output are convention-dependent (PR 2h made annotateIPFImage label
rendering branch on conv). Without threading conv through the
density config, hex/trig IPF density images silently render labels
under whatever default annotateIPFImage was given regardless of
caller intent — same silent-drop pattern that bit us in PR 2g
(PoleFigureCompositor).
PR 2k changes:
* Add HexConvention hexConvention field (default XParallelAStar) to
InversePoleFigureConfiguration_t in
Source/EbsdLib/Utilities/InversePoleFigureUtilities.h.
* LaueOps::generateAnnotatedIPFDensity now forwards
config.hexConvention into all three annotateIPFImage(...) calls.
* generateInversePoleFigure (the un-annotated variant) is unchanged:
it produces just the colored SST raster which is
convention-invariant for the Laue classes that have an inversion
fold, and convention-redundant for the others.
Tests:
* InversePoleFigureTest::AnnotatedIPFDensity_PropagatesHexConvention:
runs HexagonalOps::generateAnnotatedIPFDensity twice (X||a and
X||a*) on the same 64-orientation random Euler set, asserts the
annotated output PNGs differ byte-for-byte. Catches the silent-
drop pattern.
This is the structural piece. MTEX-comparison validation (companion
mtex_ang_to_ipf_density.m script + golden CSV + bucket-by-bucket
comparison test, mirroring the pole figure validation harness) is a
follow-up PR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per-class color-key selection now flows through a `ColorKeyKind` enum
passed at the IPF-color call site instead of a long-lived `m_ColorKey`
member. Each LaueOps subclass owns three file-local static singletons
(TSL, PUCM, NolzeHielscher) sized to its rotation point group and
fundamental sector geometry, so callers can switch styles per pixel /
per legend without mutating shared state and without having to know
which Laue class they're constructing the key for.
API shape:
- generateIPFColor(eulers, refDir, deg, ColorKeyKind kind = TSL)
- generateRodriguesColor(r1, r2, r3) (was conv-parameterized)
- generateIPFTriangleLegend(dim, full, HexConvention conv,
ColorKeyKind kind = TSL, bool gridded = false)
- computeIPFColor(eulers, refDir, deg, const IColorKey* key) -- public
Removed:
- LaueOps::setColorKey / getColorKey / setLegendRenderMode
- LaueOps::m_ColorKey member
- LegendRenderMode enum (subsumed by the `gridded` arg on legend)
- generateIPFColorImpl from the 4 hex/trig classes -- IPF color is
invariant under HexConvention, so base computeIPFColor via the
canonical X||a* getQuatSymOp tables suffices.
Audit pattern:
- HexConvention gains `NotApplicable = 0` sentinel.
- Non-hex/trig overrides default conv to NotApplicable.
- Hex/trig overrides drop the default (caller must pass conv).
- Base virtuals on conv-dependent methods have no default --
polymorphic callers must choose deliberately.
App / test plumbing:
- render_ebsd consolidates its local ColorKeyKind enum onto
ebsdlib::ColorKeyKind; installColorKey() helper is gone.
- make_ipf threads ColorKeyKind through executeAng / executeCtf /
GenerateIPFColorsImpl instead of setColorKey-loops over LaueOps.
- generate_ipf_legends passes kind=NolzeHielscher + gridded=true to
the legend call directly.
Tests:
- New ColorKeyKindTest pins the new API: enum exists, default is TSL,
PUCM/NH differ from TSL across all 11 Laue classes, legend accepts
kind + gridded.
- TSLColorKeyTest, IPFLegendTest, GriddedColorKeyTest rewritten to
use the new API instead of mutating m_ColorKey.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The vendored wlenthe coloring functions cubicToHemi and cubicLowToHemi populate function-static `std::vector<T> irho, omega` lookup tables on first call. Under ParallelDataAlgorithm (e.g. the simplnx ComputeIPFColorsFilter with PUCM kind), multiple TBB workers all see `omega.empty() == true` simultaneously, all enter the resize/iota/ partial_sum block, and the heap allocator eventually trips on a partially-published vector and aborts with `free_small_botch`. The PUCMColorKey per-class singletons in each LaueOps subclass are already serialized by C++11 magic statics during their first construction, so warm the wlenthe dispatch path once in the PUCMColorKey constructor with a benign direction. By the time any worker thread can read the lookup tables they are fully populated. Keeps the vendored wlenthe_orientation_coloring.hpp byte-identical (per the existing comment in PUCMColorKey.cpp about preserving upstream for future re-syncs). Repro: pipeline at /tmp/pucm_ipf_test.d3dpipeline (Small_IN100 .ang read → euler rotate → sample rotate → threshold → Compute IPF Colors with color_key_index=1 PUCM). Pre-fix: crash in `free_small_botch` inside cubicToHemi via 8 parallel workers. Post-fix: runs cleanly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This commit adds the ability to create Pole Figures using either the TSL X||A convention or the MTEX/Oxford X||A* convention.
This commit also adds additional color keys to the IPF color generation. The previous TSL standard is still available in addition to the new "PUCM" and "Nolze-Hielscher" color mapping (https://doi.org/10.1107/S1600576716012942)
PUCM is based off the WLenthe code base at "https://github.com/wlenthe/crystallography".
https://www.edax.com/news-events/edax-blog/edax-blog-posts/improved-ipf-color-palettes is a great description of the color mapping.
Other bugs have been fixed in this release.