Summary
Overhaul XSpect to use YAML-based configuration files as the single source of truth for analysis pipelines. Replace the current controller subclass hierarchy (6 classes with hardcoded analysis sequences) with a generic pipeline runner that dispatches steps from a YAML recipe.
Motivation
- The controller has become a graveyard for bespoke analysis sequences. Every new experiment type requires a new subclass.
- Configuration is scattered across object attributes set imperatively in notebooks.
- No reproducibility artifact exists. You can't hand someone a file and say "this is exactly what I ran."
- The controller does analysis work instead of mediating it.
Architecture
YAML as the complete recipe
One YAML file captures everything needed to reproduce an analysis from raw data:
description: "Mn Kbeta XES, time-resolved pump-probe, MFX LY69"
experiment:
hutch: xcs
experiment_id: xcsp23820
lcls_run: 21
data:
runs: [162-181, 183-190]
keys:
tt/ttCorr: time_tool_correction
epics/lxt_ttc: lxt_ttc
enc/lasDelay: encoder
ipm4/sum: ipm
tt/AMPL: time_tool_ampl
detector_keys:
epix_1/ROI_0_area:
name: epix
rois: [[0, -1]]
combine_rois: true
pipeline:
- step: filter_shots
on: xray
filter_key: ipm
threshold: 1.0e4
- step: union_shots
on: epix_ROI_1
filters: [simultaneous, laser]
- step: separate_shots
on: epix_ROI_1
filters: [xray, laser]
- step: reduce_detector_temporal
on: epix_ROI_1.simultaneous_laser
timing: timing_bin_indices.simultaneous_laser
reduction:
- step: combine_runs
- step: normalize_xes
on: summed_laser_off
output:
format: hdf5
path: ./results/
Target module layout
XSpect/
├── __init__.py
├── model/
│ ├── experiment.py # experiment, spectroscopy_experiment
│ ├── run.py # spectroscopy_run (results dict interface)
│ └── von_hamos.py # vonHamos crystal geometry
├── analysis/
│ ├── registry.py # @register_step, @register_reduction, dispatch
│ ├── spectroscopy.py # base operations (filter, union, separate, reduce)
│ ├── xes.py # XES-specific steps (normalize, energy axis, combine)
│ └── xas.py # XAS-specific steps (ccm axis, ccm binning)
├── controller/
│ ├── config_parser.py # YAML parsing + validation
│ ├── pipeline_runner.py # step dispatch loop + reduction orchestration
│ └── batch_manager.py # shot chunking, multiprocessing, reconvergence
├── visualization/
├── diagnostics/
└── postprocessing/
Phase Structure (with dependencies)
Phase 1: MVP (Static XES end-to-end)
1A: Core Infrastructure (#84)
Dependencies: None (foundational)
| Deliverable |
Description |
| model/run.py |
spectroscopy_run refactored with self.results = {} flat dict |
| model/experiment.py |
Move experiment, spectroscopy_experiment as-is |
| analysis/registry.py |
@register_step(name), @register_reduction(name), get_step(), get_reduction(), list_steps() |
| controller/config_parser.py |
YAML parse, validate required sections (experiment, data, pipeline, output), raise on unknown step names |
| controller/pipeline_runner.py |
Iterate pipeline section, look up + call steps, log execution |
| controller/batch_manager.py |
Split runs into shot ranges, multiprocessing.Pool, reconverge batch results |
| Pipeline class |
Pipeline.from_yaml(path), .run(cores, batch_size), .results |
Acceptance criteria:
- Pipeline.from_yaml("test.yaml").run(cores=4, batch_size=1000) executes on a minimal YAML with placeholder steps
- Registry discovers and dispatches decorated methods
- Batch manager splits and reconverges correctly
1B: Register Static XES Steps (#85)
Dependencies: 1A (registry must exist before steps can be registered)
| Step |
Source |
Description |
| filter_shots |
SpectroscopyAnalysis.filter_shots |
Threshold filter on diagnostic key |
| union_shots |
SpectroscopyAnalysis.union_shots |
Combine masks (AND logic) |
| separate_shots |
SpectroscopyAnalysis.separate_shots |
Exclude masks (A AND NOT B) |
| filter_detector_adu |
SpectroscopyAnalysis.filter_detector_adu |
Zero pixels below ADU threshold |
| reduce_detector_shots |
SpectroscopyAnalysis.reduce_detector_shots |
Sum detector across shot dimension |
| reduce_detector_spatial |
SpectroscopyAnalysis.reduce_detector_spatial |
ROI reduction of spatial dimension |
| apply_roi |
SpectroscopyAnalysis.apply_roi |
Apply ROI mask |
| rotate |
wraps scipy.ndimage.rotate |
Rotate detector images |
| patch_pixels |
SpectroscopyAnalysis.patch_pixels |
Bad pixel interpolation |
| hit_finding |
HitFinding.basic_detect |
Event detection + filter |
Key changes:
- All steps become stateless: no self.pixels_to_patch, no self.adu_cutoff. Parameters from YAML args only.
- Steps read/write run.results[key] instead of setattr(run, key, val).
- Dot-separated key convention: union_shots on epix_ROI_1 with filters [simultaneous, laser] writes to run.results["epix_ROI_1.simultaneous_laser"].
Acceptance criteria:
- Each step callable independently with mock run + YAML-style kwargs
- Correct output keys following dot-separated naming
- Status logs written for every step execution
1C: Static XES Integration (#86)
Dependencies: 1A + 1B (all steps registered, pipeline infrastructure runnable)
| Deliverable |
Description |
| xcsp23820_static.yaml |
Complete config reproducing XSpect_P23820.py static workflow |
| model/von_hamos.py |
Move vonHamos class to model layer |
| Numerical validation |
Run old path + new path on same data, compare outputs |
| combine_runs reduction |
Basic cross-run summation (reduction lifecycle test) |
Acceptance criteria:
- Pipeline.from_yaml("xcsp23820_static.yaml").run(cores=16, batch_size=2000) produces results matching old XESBatchAnalysisRotation.primary_analysis_static
- Execution log captures all step statuses
- Results saved to HDF5 at specified path
Phase 2: Time-Resolved XES (#87)
Dependencies: Phase 1 complete (1C passes numerical validation)
New pipeline steps
| Step |
Source |
Description |
| time_binning |
SpectroscopyAnalysis.time_binning |
Bin shots by delay stage + encoder + time tool |
| reduce_detector_temporal |
SpectroscopyAnalysis.reduce_detector_temporal |
Bin detector data into time bins |
| normalize_xes |
XESAnalysis.normalize_xes |
Area-normalize XES spectra |
| make_energy_axis |
XESAnalysis.make_energy_axis |
Energy axis from von Hamos geometry |
| droplet_reconstruction |
SpectroscopyAnalysis.droplet_reconstruction |
Sparse-to-dense detector reconstruction |
New reduction steps
| Step |
Source |
Description |
| combine_runs (full) |
XESAnalysis.combine_runs |
Sum across runs with uncertainty propagation |
| normalize_combined |
Extracted from combine_runs |
Normalize combined laser-on/off spectra |
| compute_difference |
Extracted from combine_runs |
Normalized difference spectra |
Acceptance criteria:
- Full time-resolved XES pipeline runs from YAML
- Results match current XESBatchAnalysisRotation.primary_analysis output
- Energy axis correctly generated from von Hamos parameters
Phase 3: XAS Pipelines (#88)
Dependencies: Phase 2 complete (time_binning shared between time-resolved XES and XAS)
New pipeline steps
| Step |
Source |
Description |
| make_ccm_axis |
XASAnalysis.make_ccm_axis |
CCM energy bins from setpoint values |
| ccm_binning |
XASAnalysis.ccm_binning |
Digitize shots into CCM energy bins |
| reduce_detector_ccm |
XASAnalysis.reduce_detector_ccm |
1D XAS (energy only) |
| reduce_detector_ccm_temporal |
XASAnalysis.reduce_detector_ccm_temporal |
2D XAS (energy + time) |
| bin_uniques |
SpectroscopyAnalysis.bin_uniques |
Bin by arbitrary scan variable |
Workflows replaced
| Old class |
New YAML |
| XASBatchAnalysis |
2D (energy + time) YAML |
| XASBatchAnalysis_1D_ccm |
Energy-only YAML |
| XASBatchAnalysis_1D_time |
Time-only YAML |
Acceptance criteria:
- All three XAS workflows (1D energy, 1D time, 2D energy+time) run from YAML
- Results match current XAS subclass outputs
- CCM axis handles both setpoint-derived and linspace-derived energy lists
Phase 4: Cleanup + Full Migration (#89)
Dependencies: Phases 1-3 complete (all analysis paths ported)
Scan analysis
- reduce_det_scanvar: bin detector by arbitrary scan variable
- Replaces: ScanAnalysis_1D, ScanAnalysis_1D_XES
PostProcessing integration
- Port XSpect_PostProcessing.py (exponential fitting, kinetics extraction)
- Determine: registered reduction steps vs. standalone post-hoc utilities
Visualization refactor
- XSpect_Visualization.py and XSpect_Diagnostics.py read from run.results dict
- No more getattr(run, key) pattern
Deletion
- Remove XSpect_Controller.py entirely (all 6 subclasses dead)
- Remove backwards-compat shims from init.py
Acceptance criteria:
- All current analysis workflows expressible as YAML
- Visualization works with new results format
- No remaining imports from old controller subclasses
- Old controller file deleted
Dependency Graph
1A --> 1B --> 1C --> Phase 2 --> Phase 3 --> Phase 4
| |
+-- shares time_binning -+
Critical path: registry.py (1A) -> step registration (1B) -> integration test (1C). Everything after Phase 1 is incremental step additions following the same pattern.
Inter-phase dependencies:
- Phase 2 depends on Phase 1C (pipeline must be proven end-to-end before adding complexity)
- Phase 3 depends on Phase 2 (time_binning step is shared; reduction patterns established in Phase 2 reused in Phase 3)
- Phase 4 depends on Phases 1-3 (cannot delete old code until all paths are ported)
Within Phase 1:
- 1B depends on 1A (registry must exist)
- 1C depends on 1A + 1B (needs both infrastructure and registered steps)
- 1A model/ refactoring is independent of analysis/registry.py, but both must land before 1B starts
Design Decisions
| Decision |
Resolution |
| Pipeline model |
Flat ordered list, top-to-bottom execution, no conditionals |
| Step dispatch |
Registry via @register_step(name) decorator |
| Run state |
run.results flat dict with dot-separated keys |
| on: field semantics |
String passed to step; step interprets (mask name, detector key, or derived key) |
| Parallelism |
Transparent to user. batch_size/cores at runtime, not in YAML |
| Reduction lifecycle |
Pipeline.run() has two internal phases: batch-parallel pipeline, then serial reduction across all runs |
| Backwards compat |
Old imports via shim init.py through Phases 1-3; removed in Phase 4 |
| Test data |
Small synthetic HDF5 fixture in tests/fixtures/ for unit tests |
| YAML validation |
Steps self-describe expected args; config_parser raises on unknown step names or missing required fields |
| Notebook interface |
Pipeline.from_yaml(path).run(cores=16, batch_size=2000) |
| Compute resources |
Out of YAML, passed at runtime (notebooks on HPC nodes) |
| Resumability |
Future feature, not in scope for Phases 1-4 |
What disappears
The following classes are replaced by different YAML files + one generic Pipeline class:
- XESBatchAnalysis
- XESBatchAnalysisRotation
- XASBatchAnalysis
- XASBatchAnalysis_1D_ccm
- XASBatchAnalysis_1D_time
- ScanAnalysis_1D
- ScanAnalysis_1D_XES
Sub-issues
Execution order
#84 -> #85 -> #86 (MVP complete) -> #87 -> #88 -> #89
Summary
Overhaul XSpect to use YAML-based configuration files as the single source of truth for analysis pipelines. Replace the current controller subclass hierarchy (6 classes with hardcoded analysis sequences) with a generic pipeline runner that dispatches steps from a YAML recipe.
Motivation
Architecture
YAML as the complete recipe
One YAML file captures everything needed to reproduce an analysis from raw data:
Target module layout
Phase Structure (with dependencies)
Phase 1: MVP (Static XES end-to-end)
1A: Core Infrastructure (#84)
Dependencies: None (foundational)
Acceptance criteria:
1B: Register Static XES Steps (#85)
Dependencies: 1A (registry must exist before steps can be registered)
Key changes:
Acceptance criteria:
1C: Static XES Integration (#86)
Dependencies: 1A + 1B (all steps registered, pipeline infrastructure runnable)
Acceptance criteria:
Phase 2: Time-Resolved XES (#87)
Dependencies: Phase 1 complete (1C passes numerical validation)
New pipeline steps
New reduction steps
Acceptance criteria:
Phase 3: XAS Pipelines (#88)
Dependencies: Phase 2 complete (time_binning shared between time-resolved XES and XAS)
New pipeline steps
Workflows replaced
Acceptance criteria:
Phase 4: Cleanup + Full Migration (#89)
Dependencies: Phases 1-3 complete (all analysis paths ported)
Scan analysis
PostProcessing integration
Visualization refactor
Deletion
Acceptance criteria:
Dependency Graph
Critical path: registry.py (1A) -> step registration (1B) -> integration test (1C). Everything after Phase 1 is incremental step additions following the same pattern.
Inter-phase dependencies:
Within Phase 1:
Design Decisions
What disappears
The following classes are replaced by different YAML files + one generic Pipeline class:
Sub-issues
Execution order
#84 -> #85 -> #86 (MVP complete) -> #87 -> #88 -> #89