Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Code_Review/vv/WritePoleFigure/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Binary V&V evidence is archived to OneDrive, not committed to the repo.
# The scripts + pipelines here regenerate all of it; see README.md.
*.dream3d
*.pdf
*.png
79 changes: 79 additions & 0 deletions Code_Review/vv/WritePoleFigure/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# WritePoleFigure — V&V A/B evidence (regenerated 2026-07-08)

Class-5 (expert-visual) A/B comparison for the `nx::core::WritePoleFigureFilter` (#1647). This folder
is a **full fresh regeneration** — the original working folder's legacy-input minting script and legacy
renders were unrecoverable (see `../../../vv_work/write_pole_figure/RECOVERY_STATUS.md`), so the entire
set was rebuilt here from first principles and re-run through all three engines.

> **What is committed here:** only the text artifacts — the three generator scripts, the four pipeline
> files, and this README. The **binary evidence** (input `.dream3d` files and the rendered `.pdf`/`.png`
> pole figures) is **archived to OneDrive**, not committed to the repo (see `.gitignore`). Everything is
> reproducible from the committed scripts via the steps below.

## What is compared

The same 502 orientations are rendered by three engines, in two modes, for two Laue groups:

| Engine | Runner | Output |
|--------|--------|--------|
| **DREAM3D 6.5.171** | `~/Applications/DREAM3D.app` PipelineRunner | PDF |
| **DREAM3D 6.5.172** | `Workspace3/6.5.172` PipelineRunner | PDF |
| **DREAM3D-NX** | `nxrunner` built from `topic/write_pole_figure_fixes` (#1647) | PNG |

- **Modes:** Color (`GenerationAlgorithm 0`) and Discrete (`GenerationAlgorithm 1`).
- **Laue groups:** hex (Ti, `CrystalStructures=[999,0]`) and cubic (`[999,1]`, same Eulers relabeled).

## Layout

```
input/
pf_input_hex.dream3d NX-format input, hex (502 orientations)
pf_input_cubic.dream3d NX-format input, cubic
pf_input_legacy.dream3d SIMPL v7.0 input, hex (minted by build_legacy_input.py)
pf_input_cubic_legacy.dream3d SIMPL v7.0 input, cubic
out_nx/ nx_color_1.png, nx_discrete_1.png (NX, hex)
out_cubic_nx/ nx_color_1.png, nx_discrete_1.png (NX, cubic)
out_legacy_6_5_171/ color_Phase_1.pdf, discrete_Phase_1.pdf (6.5.171, hex)
out_legacy_6_5_172/ color_Phase_1.pdf, discrete_Phase_1.pdf (6.5.172, hex)
out_cubic_legacy_6_5_171/ color_Phase_1.pdf, discrete_Phase_1.pdf (6.5.171, cubic)
out_cubic_legacy_6_5_172/ color_Phase_1.pdf, discrete_Phase_1.pdf (6.5.172, cubic)
```

## Reproduce

```bash
# 1. NX inputs are committed. Mint the legacy inputs from them:
/usr/bin/python3 build_legacy_input.py hex input/pf_input_legacy.dream3d
/usr/bin/python3 build_legacy_input.py cubic input/pf_input_cubic_legacy.dream3d

# 2. NX renders (nxrunner built from #1647):
/usr/bin/python3 gen_nx_pf.py hex && nxrunner --execute pf_nx_hex.d3dpipeline
/usr/bin/python3 gen_nx_pf.py cubic && nxrunner --execute pf_nx_cubic.d3dpipeline

# 3. Legacy renders (repeat for 172 runner and cubic variant):
/usr/bin/python3 gen_legacy_pf.py hex "$PWD/out_legacy_6_5_171" "$PWD/pf_legacy_171_hex.json"
"$DREAM3D_651/PipelineRunner" -p pf_legacy_171_hex.json
```

Scripts (all committed here): `build_legacy_input.py`, `gen_nx_pf.py`, `gen_legacy_pf.py`.

## Results

- **6.5.171 vs 6.5.172 are the same picture.** hex-color, hex-discrete, and cubic-discrete PDFs are
**byte-identical**; cubic-color differs only in a trailing PDF metadata block (identical rendering).
This confirms no pole-figure regression across the two legacy versions.
- **NX vs legacy** reproduces the expected pole-figure topology (pole positions, symmetry, color
mapping) in both color and discrete modes, for both Laue groups. NX renders to PNG rather than PDF;
compare visually.
- **Discrete mode** exercises the new `discrete_marker_radius` parameter fixed in #1647 (the develop
`nxrunner` lacked it and warned `-5433`; the #1647 build honors it). NX discrete PNGs were produced by
the branch-accurate build.

## Caveat (honesty note)

The legacy input was **reconstructed**, not recovered: `build_legacy_input.py` was rewritten from the
array contract in `gen_legacy_pf.py`'s reader proxy plus the SIMPL v7.0 HDF5 layout cloned from a
known-good legacy file. The 502 orientations themselves are the originals (extracted from the committed
`pf_input_*.dream3d`). Both legacy engines read the minted file without error and produced valid pole
figures, which validates the reconstruction. The trailing space in the phase name (`'Nickel '` in the
template; here `'Primary'`) matches the legacy convention noted in deviation D1.
86 changes: 86 additions & 0 deletions Code_Review/vv/WritePoleFigure/build_legacy_input.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#!/usr/bin/env python3
# Reconstructed 2026-07-08. The original build_legacy_input.py (which minted the legacy-format
# pf_input_legacy.dream3d for the WritePoleFigure A/B) was lost with no .pyc, so it is rebuilt here
# from (a) the array contract in gen_legacy_pf.py's reader() and (b) the exact legacy (SIMPL v7.0)
# HDF5 layout cloned from a known-good legacy file (6_6_stats_test_v2.dream3d).
#
# Extracts the 502 orientations from the NX Pole_Figure_Exemplars_v6 input and writes a
# DREAM3D 6.5.x-format .dream3d (ImageDataContainer, 502x1x1 ImageGeometry) that the 6.5.171 /
# 6.5.172 DataContainerReader can read.
#
# Usage: build_legacy_input.py <hex|cubic> <out.dream3d>
import sys
import h5py
import numpy as np

BASE = '/Users/mjackson/Workspace3/simplnx/Code_Review/vv/WritePoleFigure'
DC = 'ImageDataContainer'
ASCII = h5py.string_dtype(encoding='ascii')


def legacy_arr(group, name, data, object_type, comp, ntuples):
# Legacy cell/ensemble array: dataset stored ZYX-major as (z, y, x, comp) flattened here to
# (ntuples, comp); attrs carry x-major TupleDimensions + the SIMPL metadata.
ds = group.create_dataset(name, data=data)
ds.attrs.create('ComponentDimensions', np.array([comp], dtype=np.uint64))
ds.attrs.create('DataArrayVersion', np.array([2], dtype=np.int32))
ds.attrs['ObjectType'] = np.bytes_(object_type)
ds.attrs['Tuple Axis Dimensions'] = np.bytes_(f'x={ntuples}')
ds.attrs.create('TupleDimensions', np.array([ntuples], dtype=np.uint64))


def main():
variant = sys.argv[1]
out = sys.argv[2]
src = f'{BASE}/input/pf_input_{variant}.dream3d'

with h5py.File(src, 'r') as s:
eulers = s['DataStructure/Imported Data/Eulers'][:].reshape(-1, 3).astype(np.float32)
phases = s['DataStructure/Imported Data/Phases'][:].reshape(-1, 1).astype(np.int32)
mask = s['DataStructure/Imported Data/Mask'][:].reshape(-1, 1).astype(np.uint8)
xtal = s['DataStructure/EnsembleAttributeMatrix/CrystalStructures'][:].reshape(-1, 1).astype(np.uint32)
n = eulers.shape[0]
nens = xtal.shape[0]

with h5py.File(out, 'w') as f:
f.attrs['FileVersion'] = np.bytes_('7.0')
f.attrs['DREAM3D Version'] = np.bytes_('6.5.171')
for empty in ('DataContainerBundles', 'Montages', 'Pipeline'):
f.create_group(empty)
dcs = f.create_group('DataContainers')
dc = dcs.create_group(DC)

geom = dc.create_group('_SIMPL_GEOMETRY')
geom.attrs['GeometryName'] = np.bytes_('ImageGeometry')
geom.attrs.create('GeometryType', np.array([0], dtype=np.uint32))
geom.attrs['GeometryTypeName'] = np.bytes_('ImageGeometry')
geom.attrs.create('SpatialDimensionality', np.array([3], dtype=np.uint32))
geom.attrs.create('UnitDimensionality', np.array([3], dtype=np.uint32))
geom.create_dataset('DIMENSIONS', data=np.array([n, 1, 1], dtype=np.int64))
geom.create_dataset('ORIGIN', data=np.array([0, 0, 0], dtype=np.float32))
geom.create_dataset('SPACING', data=np.array([1, 1, 1], dtype=np.float32))

cell = dc.create_group('CellData')
cell.attrs.create('AttributeMatrixType', np.array([3], dtype=np.uint32))
cell.attrs.create('TupleDimensions', np.array([n, 1, 1], dtype=np.uint64))
legacy_arr(cell, 'Eulers', eulers, 'DataArray<float>', 3, n)
legacy_arr(cell, 'Phases', phases, 'DataArray<int32_t>', 1, n)
legacy_arr(cell, 'Mask', mask, 'DataArray<bool>', 1, n)

ens = dc.create_group('CellEnsembleData')
ens.attrs.create('AttributeMatrixType', np.array([11], dtype=np.uint32))
ens.attrs.create('TupleDimensions', np.array([nens], dtype=np.uint64))
legacy_arr(ens, 'CrystalStructures', xtal, 'DataArray<uint32_t>', 1, nens)
names = np.array(['Invalid Phase'] + ['Primary'] * (nens - 1), dtype=object)
mn = ens.create_dataset('MaterialName', shape=(nens,), dtype=ASCII, data=names)
mn.attrs.create('ComponentDimensions', np.array([1], dtype=np.uint64))
mn.attrs.create('DataArrayVersion', np.array([2], dtype=np.int32))
mn.attrs['ObjectType'] = np.bytes_('StringDataArray')
mn.attrs['Tuple Axis Dimensions'] = np.bytes_(f'x={nens}')
mn.attrs.create('TupleDimensions', np.array([nens], dtype=np.uint64))

print(f'wrote {out} ({n} orientations, {nens} ensembles, variant={variant})')


if __name__ == '__main__':
main()
116 changes: 116 additions & 0 deletions Code_Review/vv/WritePoleFigure/gen_legacy_pf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
#!/usr/bin/env python3
# Regenerated 2026-07-08 for Code_Review/vv/WritePoleFigure. Legacy (SIMPL 6.5.x) side of the A/B.
# Reconstructed from the decompiled gen_legacy_pf.cpython-312.pyc; paths retargeted to this folder
# and parameterized for hex/cubic. Builds a SIMPL pipeline (DataContainerReader + legacy
# WritePoleFigure x2: color + discrete) that the DREAM3D 6.5.171 / 6.5.172 PipelineRunner executes.
import json
import sys

BASE = '/Users/mjackson/Workspace3/simplnx/Code_Review/vv/WritePoleFigure'
DC = 'ImageDataContainer'


def da(name, path, obj_type, comps):
return {
'Component Dimensions': [comps],
'Flag': 2,
'Name': name,
'Object Type': obj_type,
'Path': path,
'Tuple Dimensions': [1],
'Version': 2,
}


def reader(in_file):
cellpath = f'/DataContainers/{DC}/CellData'
enspath = f'/DataContainers/{DC}/CellEnsembleData'
proxy = {
'Data Containers': [
{
'Attribute Matricies': [
{
'Data Arrays': [
da('Eulers', cellpath, 'DataArray<float>', 3),
da('Phases', cellpath, 'DataArray<int32_t>', 1),
da('Mask', cellpath, 'DataArray<bool>', 1),
],
'Flag': 2,
'Name': 'CellData',
'Type': 3,
},
{
'Data Arrays': [
da('CrystalStructures', enspath, 'DataArray<uint32_t>', 1),
da('MaterialName', enspath, 'StringDataArray', 1),
],
'Flag': 2,
'Name': 'CellEnsembleData',
'Type': 11,
},
],
'Flag': 2,
'Geometry': {'Geometry_Type': 0, 'Geometry_Type_Name': 'ImageGeometry'},
'Name': DC,
}
],
'Version': 6,
}
return {
'FilterVersion': '6.5.171',
'Filter_Enabled': True,
'Filter_Human_Label': 'Read DREAM.3D Data File',
'Filter_Name': 'DataContainerReader',
'Filter_Uuid': '{043cbde5-3878-5718-958f-ae75714df0df}',
'InputFile': in_file,
'OverwriteExistingDataContainers': False,
'InputFileDataContainerArrayProxy': proxy,
}


def dap(am, name):
return {'Data Container Name': DC, 'Attribute Matrix Name': am, 'Data Array Name': name}


def write_pf(out_dir, prefix, gen_algo, use_mask):
return {
'FilterVersion': '6.5.171',
'Filter_Enabled': True,
'Filter_Human_Label': 'Export Pole Figure Images',
'Filter_Name': 'WritePoleFigure',
'Filter_Uuid': '{a10bb78e-fcff-553d-97d6-830a43c85385}',
'CellEulerAnglesArrayPath': dap('CellData', 'Eulers'),
'CellPhasesArrayPath': dap('CellData', 'Phases'),
'CrystalStructuresArrayPath': dap('CellEnsembleData', 'CrystalStructures'),
'MaterialNameArrayPath': dap('CellEnsembleData', 'MaterialName'),
'GoodVoxelsArrayPath': dap('CellData', 'Mask'),
'UseGoodVoxels': 1 if use_mask else 0,
'GenerationAlgorithm': gen_algo,
'ImageLayout': 0,
'ImageSize': 512,
'LambertSize': 64,
'NumColors': 32,
'OutputPath': out_dir,
'ImagePrefix': prefix,
'Title': 'WritePoleFigure AB',
}


if __name__ == '__main__':
variant = sys.argv[1] if len(sys.argv) > 1 else 'hex' # 'hex' or 'cubic'
out_dir = sys.argv[2]
out_json = sys.argv[3]
in_file = f'{BASE}/input/pf_input_legacy.dream3d' if variant == 'hex' else f'{BASE}/input/pf_input_cubic_legacy.dream3d'
pipe = {
'0': reader(in_file),
'1': write_pf(out_dir, 'color_', 0, False),
'2': write_pf(out_dir, 'discrete_', 1, False),
'PipelineBuilder': {
'Name': 'WritePoleFigure legacy AB',
'Number_Filters': 3,
'Version': 6,
},
}
with open(out_json, 'w') as fh:
json.dump(pipe, fh, indent=2)
print('wrote', out_json, '-> output dir', out_dir)
75 changes: 75 additions & 0 deletions Code_Review/vv/WritePoleFigure/gen_nx_pf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/usr/bin/env python3
# Regenerated 2026-07-08 for Code_Review/vv/WritePoleFigure. NX side of the WritePoleFigure A/B.
# Reconstructed from the decompiled gen_nx_pf.cpython-312.pyc; paths retargeted to this folder and
# parameterized for the hex and cubic inputs. Builds a DREAM3D-NX pipeline (ReadDREAM3DFilter +
# WritePoleFigure x2: Color + Discrete) and writes it next to this script.
import json
import sys

BASE = '/Users/mjackson/Workspace3/simplnx/Code_Review/vv/WritePoleFigure'


def a(v):
return {'value': v, 'version': 1}


def wpf(prefix, gen_algo, geom_name, outdir):
return {
'args': {
'title': a('WritePoleFigure AB'),
'generation_algorithm_index': a(gen_algo),
'lambert_size': a(64),
'num_colors': a(32),
'discrete_marker_radius': a(3),
'image_layout_index': a(0),
'image_size': a(512),
'output_path': a(outdir),
'image_prefix': a(prefix),
'use_mask': a(False),
'cell_euler_angles_array_path': a('Imported Data/Eulers'),
'cell_phases_array_path': a('Imported Data/Phases'),
'mask_array_path': a('Imported Data/Mask'),
'crystal_structures_array_path': a('EnsembleAttributeMatrix/CrystalStructures'),
'material_name_array_path': a('EnsembleAttributeMatrix/PhaseNames'),
'save_as_image_geometry': a(True),
'write_image_to_disk': a(True),
'output_image_geometry_path': a(geom_name),
'save_intensity_plots': a(False),
'normalize_to_mrd': a(True),
'intensity_geometry_path': a(geom_name + ' Intensity'),
'intensity_plot_1_name': a('<0001>'),
'intensity_plot_2_name': a('<11-20>'),
'intensity_plot_3_name': a('<10-10>'),
'hex_convention_index': a(0),
},
'comments': '',
'filter': {'name': 'nx::core::WritePoleFigureFilter', 'uuid': '00cbb97e-a5c2-43e6-9a35-17a0f9ce26ed'},
'isDisabled': False,
}


def pipeline(in_file, outdir):
return {
'name': 'WritePoleFigure NX AB',
'pipeline': [
{
'args': {'import_data_object': a({'file_path': in_file, 'data_paths': None})},
'comments': '',
'filter': {'name': 'nx::core::ReadDREAM3DFilter', 'uuid': '0dbd31c7-19e0-4077-83ef-f4a6459a0e2d'},
'isDisabled': False,
},
wpf('nx_color_', 0, 'PoleFigure_Color', outdir),
wpf('nx_discrete_', 1, 'PoleFigure_Discrete', outdir),
],
'version': 1,
}


if __name__ == '__main__':
variant = sys.argv[1] if len(sys.argv) > 1 else 'hex' # 'hex' or 'cubic'
in_file = f'{BASE}/input/pf_input_{variant}.dream3d'
outdir = f'{BASE}/out_nx' if variant == 'hex' else f'{BASE}/out_cubic_nx'
out_pipe = f'{BASE}/pf_nx_{variant}.d3dpipeline'
with open(out_pipe, 'w') as fh:
json.dump(pipeline(in_file, outdir), fh, indent=2)
print('wrote', out_pipe, '-> output dir', outdir)
Loading
Loading