Skip to content

Commit fa571af

Browse files
committed
Add EM spot-ownership model (Phase 1 Track C) to pf-HEDM pipeline.
Replaces hard spot-to-grain assignment with soft probabilistic ownership via Expectation-Maximization. Key improvements: ring-filtered E-step prevents cross-ring misassignment, angular wrapping handles omega/eta at ±180 deg boundaries, weighted sinograms preserve shared-spot intensity (critical for twins) while suppressing noise. Pipeline flow: pre-tomo refinement → EM (E-step + M-step) → weighted sinograms → tomo. New -useEM 1 flag on existing doTomo path.
1 parent f191997 commit fa571af

5 files changed

Lines changed: 453 additions & 58 deletions

File tree

FF_HEDM/workflows/pf_MIDAS.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -977,6 +977,22 @@ def check_error_file(filename):
977977
parser.add_argument('-reconMethod', type=str, required=False, default='fbp', choices=['fbp', 'mlem', 'osem'], help='Sinogram reconstruction method: fbp=filtered back-projection via gridrec (default), mlem=Maximum Likelihood EM (handles sparse/missing angles natively), osem=Ordered Subsets EM (accelerated MLEM).')
978978
parser.add_argument('-mlemIter', type=int, required=False, default=50, help='Number of MLEM/OSEM iterations (only used when -reconMethod is mlem or osem). Default: 50.')
979979
parser.add_argument('-osemSubsets', type=int, required=False, default=4, help='Number of ordered subsets for OSEM (only used when -reconMethod is osem). Default: 4.')
980+
parser.add_argument('-useEM', type=int, required=False, default=0,
981+
help='Use EM spot-ownership for soft sinogram generation (requires doTomo=1). Default: 0 (off).')
982+
parser.add_argument('-emIter', type=int, required=False, default=10,
983+
help='Number of EM iterations. Default: 10.')
984+
parser.add_argument('-emSigmaInit', type=float, required=False, default=0.02,
985+
help='Initial sigma for EM Gaussian kernel (radians, ~1 degree). Default: 0.02.')
986+
parser.add_argument('-emSigmaMin', type=float, required=False, default=0.005,
987+
help='Minimum sigma for EM annealing floor (radians). Default: 0.005.')
988+
parser.add_argument('-emSigmaDecay', type=float, required=False, default=0.9,
989+
help='Sigma decay factor per EM iteration. Default: 0.9.')
990+
parser.add_argument('-emRefineOrientations', type=int, required=False, default=1,
991+
help='Whether EM M-step refines grain orientations. 0=E-step only, 1=full EM (default).')
992+
parser.add_argument('-emOptSteps', type=int, required=False, default=5,
993+
help='Gradient steps per EM M-step per grain. Default: 5.')
994+
parser.add_argument('-emLR', type=float, required=False, default=0.005,
995+
help='Learning rate for EM M-step Adam optimizer. Default: 0.005.')
980996
parser.add_argument('-resume', type=str, required=False, default='',
981997
help='Path to a pipeline H5 file to resume from. Auto-detects the first incomplete stage.')
982998
parser.add_argument('-restartFrom', type=str, required=False, default='',
@@ -1008,6 +1024,7 @@ def check_error_file(filename):
10081024
sinoType = args.sinoType
10091025
sinoSource = args.sinoSource
10101026
sinoMode = 1 if sinoSource == 'indexing' else 0
1027+
useEM = args.useEM
10111028
# Ensure command line file arguments are absolute paths before a potential directory change
10121029
param_path = os.path.abspath(args.paramFile)
10131030
param_dir = os.path.dirname(param_path)
@@ -1575,6 +1592,59 @@ def _should_run(stage_name):
15751592
if idData[voxNr, 1] != 0:
15761593
f.write(f"{idData[voxNr, 0]} {idData[voxNr, 1]} {idData[voxNr, 2]} {idData[voxNr, 3]} {idData[voxNr, 4]}\n")
15771594

1595+
# EM spot-ownership: pre-tomo refinement + soft sinograms
1596+
if useEM == 1 and doTomo == 1:
1597+
logger.info("=== EM Spot-Ownership Pipeline ===")
1598+
1599+
# Step 1: Pre-tomo refinement to get better initial orientations
1600+
logger.info("Running pre-tomo refinement for EM initialization")
1601+
1602+
# Save first-pass Output/Results before refinement overwrites
1603+
for dirn in ['Results']:
1604+
if os.path.isdir(dirn):
1605+
shutil.rmtree(dirn)
1606+
os.makedirs(dirn, exist_ok=True)
1607+
1608+
resRefine0 = []
1609+
for nodeNr in range(nNodes):
1610+
resRefine0.append(refinescanning(topdir, numProcs, midas_path,
1611+
blockNr=nodeNr, numBlocks=nNodes,
1612+
useGPU=useGPU))
1613+
outputRefine0 = [i.result() for i in resRefine0]
1614+
for i, output in enumerate(outputRefine0):
1615+
if output and "Failed" in output:
1616+
logger.warning(f"Pre-tomo refinement warning for node {i}: {output}")
1617+
1618+
logger.info("Pre-tomo refinement complete. Updating grain orientations.")
1619+
1620+
# Step 2: Re-derive unique grain orientations from refined results
1621+
from em_pf_integration import update_unique_orientations_from_refinement
1622+
update_unique_orientations_from_refinement(topdir, nScans)
1623+
1624+
# Step 3: Run EM spot-ownership
1625+
logger.info("Running EM spot-ownership for weighted sinograms")
1626+
from em_pf_integration import run_em_spot_ownership
1627+
1628+
# Remove old sinogram files before EM writes new ones
1629+
for pattern in ["sinos_*.bin", "omegas_*.bin", "nrHKLs_*.bin"]:
1630+
for old_f in glob.glob(pattern):
1631+
os.remove(old_f)
1632+
1633+
run_em_spot_ownership(
1634+
topdir=topdir,
1635+
n_scans=nScans,
1636+
n_iter=getattr(args, 'emIter', 10),
1637+
sigma_init=getattr(args, 'emSigmaInit', 0.02),
1638+
sigma_min=getattr(args, 'emSigmaMin', 0.005),
1639+
sigma_decay=getattr(args, 'emSigmaDecay', 0.9),
1640+
n_opt_steps=getattr(args, 'emOptSteps', 5),
1641+
lr=getattr(args, 'emLR', 0.005),
1642+
refine_orientations=bool(getattr(args, 'emRefineOrientations', 1)),
1643+
use_refined_orientations=True,
1644+
)
1645+
1646+
logger.info("EM complete. Proceeding with tomo reconstruction on EM sinograms.")
1647+
15781648
# Run tomography if requested
15791649
if doTomo == 1:
15801650
# Find sino file

0 commit comments

Comments
 (0)