Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
f6fe291
bump: pydantify NexusValuesWriter
keara-soloway Mar 25, 2026
2832f19
add: NexusValuesWriter.resize_axis
keara-soloway Mar 25, 2026
0c1e714
fix: list access typo
keara-soloway Mar 25, 2026
f02dd10
add: expandable first dimension for all NXfields in result of MapProc…
keara-soloway Mar 25, 2026
2ef0936
add: reminder comment
keara-soloway Mar 26, 2026
a318c74
fix: slice stop indices in MapSliceProcessor
keara-soloway Mar 26, 2026
b0c2e10
fix: support oddball EDD get_detector_data return values
keara-soloway Mar 26, 2026
9fa6985
add: support for finding SCAN_N datasets in groups other than "data"
keara-soloway Mar 26, 2026
4fcdda1
fix: add independent_dimensions to results of MapSliceProcessor
keara-soloway Apr 1, 2026
9ed9e01
fix: EDD-specific modifications for MapSliceProcessor
keara-soloway Apr 1, 2026
76a9a61
fix: some missing resizable NXfields for MapProcessor
keara-soloway Apr 1, 2026
a68706c
fix: independent_dimensions paths in MapSliceProcessor results
keara-soloway Apr 1, 2026
4d68997
add: common.models.IndexSliceConfig
keara-soloway Apr 6, 2026
0ec1f60
add: NexusValuesWriter.idx_slice
keara-soloway Apr 6, 2026
8fd846b
add: MapProcessor.remove_constant_dims
keara-soloway Apr 6, 2026
3205a4c
fix: include independent_dimensions.index in results of MapSliceProce…
keara-soloway Apr 6, 2026
3791322
fix: bug with MapProcessor.remove_constant_dims
keara-soloway Apr 6, 2026
6ad4022
add: link to all scalar_data fields from main <map_nxentry>.data NXda…
keara-soloway Apr 6, 2026
5997938
fix: add data type check/conversion to NexusValuesWriter
keara-soloway Apr 6, 2026
06ce58f
fix: peak_fit_info in StrainAnalysisProcessor when setup is True
keara-soloway Apr 6, 2026
3cbcb31
add: make all NXfields resizable in StrainAnalysisProcessor results
keara-soloway Apr 6, 2026
f13a81b
temporary: add OrnlStrainAnalysisProcessor
keara-soloway Apr 10, 2026
00f8b58
optimize: NexusValuesWriter loop order
keara-soloway Apr 10, 2026
a2ad853
add: StrainAnalysisProcessor.standalone
keara-soloway Apr 21, 2026
b0c9ae5
optimize: edd.SliceNXdataReader
keara-soloway Apr 21, 2026
4043b03
Merge pull request #284 from CHESSComputing/main
rolfverberg Apr 28, 2026
23ba5d3
add: added peak fit results to the EDD calibration output yamls
rolfverberg Apr 28, 2026
5d229a8
fix: YAMLWriter can write lists as well as dicts
keara-soloway Apr 28, 2026
4f85f99
add: JSONWriter
keara-soloway Apr 28, 2026
b81e6c0
add: StrainAnalysisProcessor.json_results
keara-soloway Apr 28, 2026
925f136
add: scan_number option for PointByPointScanData.data_type
keara-soloway Apr 29, 2026
58b40a4
fix: allow MapConfig.scalar_data to contain items with data_type==exp…
keara-soloway Apr 29, 2026
f868c51
fix: allow 1d detector shapes in DetectorConfig
keara-soloway Apr 29, 2026
519f989
feat: support 0-scan MapConfigs
keara-soloway Apr 30, 2026
168f0bb
add: JSONWriter.update and JSONWriter.extend
keara-soloway Apr 30, 2026
e8ff4e3
test: Updated tomo_one_plane.py to use the hollow_cube example
rolfverberg Apr 30, 2026
7610912
test: renamed tomo script consistent with CHAP pipeline version
rolfverberg Apr 30, 2026
a27102a
feat: support processing multiple scans with MapSliceProcessor
keara-soloway Apr 30, 2026
9cf6b79
add: support reading data from multiple scans with edd.SliceNXdataReader
keara-soloway Apr 30, 2026
9fba14e
add: simplified script based calling and added an EDD calib script
rolfverberg May 1, 2026
4f4ce62
docs: cleaned up kwargs docstrings
rolfverberg May 1, 2026
d46b27d
fix: fixed pipelineitem.run for duplicate class names
rolfverberg May 1, 2026
661e81d
feat: support 0-scan maps in edd.StrainAnalysisProcessor with setup=T…
keara-soloway May 1, 2026
fc796f0
cut: exclude spectra-like data from JSON results of StrainAnalysisPro…
keara-soloway May 1, 2026
02eae41
add: include placeholder values for unused peaks in JSON results of S…
keara-soloway May 1, 2026
266086e
fix: peak fit results for the EDD calibration in channel energies
rolfverberg May 5, 2026
10d14f6
Merge pull request #286 from rolfverberg/dev
rolfverberg May 6, 2026
d248941
fix: missing call to import_scanparser in MapConfig validation
May 13, 2026
3b76067
fix: missing call to import_scanparser in MapConfig validation
rolfverberg May 13, 2026
dbbb41c
Merge pull request #288 from rolfverberg/dev
rolfverberg May 13, 2026
278eb49
fix: saxswaxs processor imports & docstring typo
keara-soloway May 14, 2026
8d4d5a4
fix: erroneous in-place modification of pipeline data in UpdateValues…
keara-soloway May 14, 2026
c75303e
fix: typo
keara-soloway May 14, 2026
f8e5c16
Merge remote-tracking branch 'origin/dev' into edd_ornl
keara-soloway May 14, 2026
503fe38
Merge pull request #290 from CHESSComputing/edd_ornl
keara-soloway May 15, 2026
acf8b1d
fix: module path to IndexSliceConfig
keara-soloway May 15, 2026
f9e9cfb
add: ZarrValuesWriter.resize_axis and ZarrValuesWriter.idx_slice
keara-soloway May 15, 2026
d665f2f
fix: preserve NXlinks when passing through NexusToZarrProcessor and Z…
keara-soloway May 15, 2026
72cc371
bump: pydantify idx_slice parameter in common.map_utils.MapSliceProce…
keara-soloway May 15, 2026
a5d616a
rm: idx_slices parameter for saxswaxs.PyfaiIntegrationProcessor
keara-soloway May 15, 2026
1401044
feat: support 0-sized setup arrays in saxswaxs.SetupProcessor
keara-soloway May 15, 2026
eb06d7b
fix: do not duplicate log handlers for PipelineItems with the same name
keara-soloway May 18, 2026
d6587dd
style: include lineno in log formatters
keara-soloway May 18, 2026
47b24e0
add: support detector_config in addition to detectors field in MapSli…
keara-soloway May 21, 2026
ea2510c
feat: in zarr_tree methods of pyFAI integration configs, support addi…
keara-soloway May 21, 2026
740072b
doc: update MapSliceProcessor docstring
keara-soloway May 21, 2026
1fda1d3
feat: support slicewise processing for corrected data
keara-soloway May 21, 2026
8fc3d6e
fix: set logger handlers instead of using addHandler (avoids duplicat…
keara-soloway May 21, 2026
f899e7c
feat: added create_copy to NexusReader to avoid linked data issues
rolfverberg May 22, 2026
0b0f2f4
fix: squeeze empty first dim out of I_background before putting in za…
keara-soloway May 22, 2026
afc0279
feat: alias old CorrectionConfig field names
keara-soloway May 22, 2026
6fe7c82
fix: add default values for optional fields sample_thickness_cm and s…
keara-soloway May 22, 2026
dc3d5f9
fix: swap old method args for new config fields in saxswaxs.*Correcti…
keara-soloway May 22, 2026
1fb4b1f
fix: include background intensities in input data for correction Proc…
keara-soloway May 22, 2026
e8f16c9
add: Added a max # func evals for option for the fit processor
rolfverberg May 28, 2026
580d596
fix: bug in max_nfev kwargs for scipy fitting
rolfverberg May 29, 2026
2bc6f6b
Merge pull request #293 from rolfverberg/dev
keara-soloway Jun 1, 2026
6e0bc65
fix: add CHAP.saxswaxs.models.Background.idx_slice
keara-soloway Jun 1, 2026
ad7de50
fix: changed config to FitProcessor class field, change input data ty…
rolfverberg Jun 2, 2026
fc955a1
Merge branch 'saxswaxs' into dev
keara-soloway Jun 2, 2026
372c615
fix: merge commit bug
keara-soloway Jun 2, 2026
3de02fa
fix: merge more fit processor commit bug
rolfverberg Jun 2, 2026
00acad1
Merge pull request #295 from CHESSComputing/saxswaxs
rolfverberg Jun 2, 2026
cf1e088
refactor: change valid input data types of FitProcessor and Fit/FitMap
rolfverberg Jun 9, 2026
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
307 changes: 236 additions & 71 deletions CHAP/common/map_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@
from pydantic import (
conint,
conlist,
model_validator,
Field,
FilePath,
)
from typing import Optional

# Local modules
from CHAP.common.models.common import IndexSliceConfig
from CHAP.common.models.map import (
DetectorConfig,
Detector,
MapConfig,
)
Expand Down Expand Up @@ -54,44 +58,50 @@ class MapSliceProcessor(Processor):

:ivar map_config: Map configuration.
:vartype map_config: MapConfig
:ivar detectors: Detector configurations.
:vartype detectors:
list[:class:`~CHAP.common.models.map.Detector`]
:ivar detector_config: Detector configurations.
:vartype detector_config: :class:`~CHAP.common.models.map.DetectorConfig`
:ivar spec_file: SPEC file containing scan from which to read a
slice of raw data.
:vartype spec_file: str
:ivar scan_number: Number of scan from which to read a slice of
:vartype spec_file: pydantic.FilePath
:ivar scan_numbers: Numbers of scans from which to read slices of
raw data.
:vartype scan_number: int
:vartype scan_numbers: list[int]
:ivar idx_slice: Parameters for the slice of each scan to process
Defaults to `IndexSliceConfig()`.
:vartype idx_slice: CHAP.common.models.common.IndexSliceConfig, optional
"""

pipeline_fields: dict = Field(
default={
'map_config': 'common.models.map.MapConfig',
'detector_config': 'common.models.map.DetectorConfig'
},
init_var=True)
map_config: MapConfig
detectors: conlist(item_type=Detector, min_length=1)
detector_config: DetectorConfig
detectors: Optional[conlist(item_type=Detector)] = None
spec_file: FilePath
scan_number: conint(gt=0)
scan_number: Optional[conint(gt=0)] = None
scan_numbers: Optional[conlist(item_type=conint(gt=0))] = None
idx_slice: Optional[IndexSliceConfig] = IndexSliceConfig()

def process(self, data, #spec_file, scan_number,
idx_slice={'start': 0, 'step': 1}):
"""Aggregate partial spec and detector data from one scan in a
map, returning results in a format suitable for writing to the
full map container with
def process(self, data):

"""Aggregate partial spec and detector data from one or more
scans in a map, returning results in a format suitable for
writing to the full map container with
:class:`~CHAP.common.writer.NexusValuesWriter` or
:class:`~CHAP.common.writer.ZarrValuesWriter`.

When all scans are adjacent in the map and `idx_slice` covers
each scan in full, data_points entries for the same path are
consolidated into a single array + slice, avoiding redundant
write calls.

:param data: Result of `Reader.read` where at least one item
has the value `'common.models.map.MapConfig'` for the
`'schema'` key.
:type data: list[PipelineData]
:type idx_slice: Parameters for the slice of the scan to
process (slice parameters are the usual for the python
`slice` object: `'start'`, `'stop'`, and
`'step'`). Defaults to `{'start': 0, 'step': '1'}`.
:type idx_slice: dict[str, int], optional
:return: Slice of map data, ready to be written to a map
container.
:rtype: list[dict[str, Any]]
Expand All @@ -103,68 +113,223 @@ def process(self, data, #spec_file, scan_number,
import numpy as np

# Local modules
from chess_scanparsers import choose_scanparser
from CHAP.common.models.map import SpecScans

ScanParser = choose_scanparser(
self.map_config.station, self.map_config.experiment_type)
scans = SpecScans(
spec_file=self.spec_file, scan_numbers=[self.scan_number])
scan = scans.get_scanparser(self.scan_number)

# Get index offset for this data slice within the map
npts_scan = int(scan.spec_scan_npts)
nscans_prev = 0
for scans in self.map_config.spec_scans:
for scan_n in scans.scan_numbers:
if (os.path.abspath(self.spec_file) == \
os.path.abspath(self.spec_file)
and scan_n == self.scan_number):
scans_obj = SpecScans(
spec_file=self.spec_file, scan_numbers=self.scan_numbers)
self_spec_file_abs = os.path.abspath(str(self.spec_file))

if self.map_config.experiment_type == 'EDD':
def get_detector_data(scan, detector, index):
return scan.get_detector_data(detector.get_id(), index)[0][0]
else:
def get_detector_data(scan, detector, index):
return scan.get_detector_data(detector.get_id(), index)

# Build flat ordered list of (abs_spec_file, scan_number) for
# all scans in the map to determine each scan's map position
map_scan_order = []
for spec_scans_item in self.map_config.spec_scans:
sf_abs = os.path.abspath(str(spec_scans_item.spec_file))
for sn in spec_scans_item.scan_numbers:
map_scan_order.append((sf_abs, sn))
scan_positions = {}
for sn in self.scan_numbers:
for pos, (sf, n) in enumerate(map_scan_order):
if sf == self_spec_file_abs and n == sn:
scan_positions[sn] = pos
break
nscans_prev += 1
index_offset = nscans_prev * npts_scan

# Get spec scan indices to process
scan_indices = range(npts_scan)[slice(
idx_slice.get('start', 0),
idx_slice.get('stop', npts_scan + 1),
idx_slice.get('step', 1)
)]
# Get map indices to write to
map_indices = slice(
idx_slice.get('start', 0) + index_offset,
idx_slice.get('stop', npts_scan + 1) + index_offset,
idx_slice.get('step', 1)

# Process scans in map order
sorted_scan_numbers = sorted(
self.scan_numbers, key=lambda sn: scan_positions[sn])

slice_start = self.idx_slice._slice.start
slice_step = self.idx_slice._slice.step

# Collect per-scan metadata; assumes uniform npts across scans
# for index_offset calculation (index_offset = map_pos * npts)
per_scan = []
for sn in sorted_scan_numbers:
scan = scans_obj.get_scanparser(sn)
npts_scan = int(scan.spec_scan_npts)
index_offset = scan_positions[sn] * npts_scan
# Cap stop at npts_scan so map_indices and data stay in sync
slice_stop = min(self.idx_slice._slice.stop, npts_scan) \
if self.idx_slice._slice.stop > 0 else npts_scan
scan_indices = range(npts_scan)[
slice(slice_start, slice_stop, slice_step)]
map_indices = slice(
slice_start + index_offset,
slice_stop + index_offset,
slice_step,
)
per_scan.append({
'sn': sn, 'scan': scan, 'npts_scan': npts_scan,
'index_offset': index_offset,
'scan_indices': scan_indices,
'map_indices': map_indices,
'full': (slice_start == 0
and slice_step == 1
and slice_stop == npts_scan),
})

# Consolidate into single data_points entries when all scans
# are adjacent in the map and idx_slice covers each scan fully
sorted_positions = [scan_positions[sn] for sn in sorted_scan_numbers]
scans_are_adjacent = all(
sorted_positions[i + 1] == sorted_positions[i] + 1
for i in range(len(sorted_positions) - 1)
)
can_consolidate = (
len(per_scan) > 1
and scans_are_adjacent
and all(ps['full'] for ps in per_scan)
)

data_points = [
{
'path': f'{self.map_config.title}/scalar_data/{s_d.label}',
'data': np.asarray([
s_d.get_value(
scans, self.scan_number, i,
scalar_data=self.map_config.scalar_data)
for i in scan_indices
]),
'idx': map_indices
}
for s_d in self.map_config.all_scalar_data
]
data_points.extend(
[
{
'path': f'{self.map_config.title}/data/{det.get_id()}',
if can_consolidate:
first, last = per_scan[0], per_scan[-1]
merged_idx = slice(
first['index_offset'],
last['index_offset'] + last['npts_scan'],
1,
)
data_points = [{
'path': (f'{self.map_config.title}'
f'/independent_dimensions/index'),
'data': np.arange(
first['index_offset'],
last['index_offset'] + last['npts_scan'],
),
'idx': merged_idx,
}]
for s_d in self.map_config.all_scalar_data:
data_points.append({
'path': (f'{self.map_config.title}'
f'/scalar_data/{s_d.label}'),
'data': np.concatenate([
np.asarray([
s_d.get_value(
scans_obj, ps['sn'], i,
scalar_data=self.map_config.scalar_data)
for i in ps['scan_indices']
])
for ps in per_scan
]),
'idx': merged_idx,
})
for dim in self.map_config.independent_dimensions:
data_points.append({
'path': (f'{self.map_config.title}'
f'/independent_dimensions/{dim.label}'),
'data': np.concatenate([
np.asarray([
dim.get_value(
scans_obj, ps['sn'], i,
scalar_data=self.map_config.scalar_data)
for i in ps['scan_indices']
])
for ps in per_scan
]),
'idx': merged_idx,
})
for det in self.detector_config.detectors:
data_points.append({
'path': (f'{self.map_config.title}'
f'/data/{det.get_id()}'),
'data': np.concatenate([
np.asarray([
get_detector_data(ps['scan'], det, i)
for i in ps['scan_indices']
])
for ps in per_scan
]),
'idx': merged_idx,
})
else:
data_points = []
for ps in per_scan:
data_points.append({
'path': (f'{self.map_config.title}'
f'/independent_dimensions/index'),
'data': np.asarray(
[ps['index_offset'] + i for i in ps['scan_indices']]
),
'idx': ps['map_indices'],
})
data_points.extend([{
'path': (f'{self.map_config.title}'
f'/scalar_data/{s_d.label}'),
'data': np.asarray([
scan.get_detector_data(det.get_id(), i)
for i in scan_indices
s_d.get_value(
scans_obj, ps['sn'], i,
scalar_data=self.map_config.scalar_data)
for i in ps['scan_indices']
]),
'idx': map_indices
}
for det in self.detectors
]
)
'idx': ps['map_indices'],
} for s_d in self.map_config.all_scalar_data])
data_points.extend([{
'path': (f'{self.map_config.title}'
f'/independent_dimensions/{dim.label}'),
'data': np.asarray([
dim.get_value(
scans_obj, ps['sn'], i,
scalar_data=self.map_config.scalar_data)
for i in ps['scan_indices']
]),
'idx': ps['map_indices'],
} for dim in self.map_config.independent_dimensions])
data_points.extend([{
'path': (f'{self.map_config.title}'
f'/data/{det.get_id()}'),
'data': np.asarray([
get_detector_data(ps['scan'], det, i)
for i in ps['scan_indices']
]),
'idx': ps['map_indices'],
} for det in self.detector_config.detectors])
return data_points

@model_validator(mode='before')
@classmethod
def fill_scan_numbers(cls, data):
if not isinstance(data, dict):
return data
if 'scan_numbers' not in data or data['scan_numbers'] is None:
if data.get('scan_number') is not None:
data['scan_numbers'] = [data['scan_number']]
elif isinstance(data['scan_numbers'], int):
data['scan_numbers'] = [data['scan_numbers']]
elif isinstance(data['scan_numbers'], str):
from CHAP.utils.general import string_to_list
data['scan_numbers'] = string_to_list(data['scan_numbers'])
return data

@model_validator(mode='after')
def validate_scan_numbers(self):
if self.scan_numbers is None:
raise ValueError(
'scan_numbers is required; alternatively, provide scan_number')
if self.scan_number is not None \
and self.scan_number not in self.scan_numbers:
self.scan_numbers.append(self.scan_number)
return self

@model_validator(mode='before')
def fill_detector_config(cls, data):
if not isinstance(data, dict):
return data
if 'detector_config' not in data or data['detector_config'] is None:
if data.get('detectors') is not None:
data['detector_config'] = DetectorConfig(
detectors=data['detectors']
)
else:
raise ValueError(
'detector_config is required; alternatively, provide detectors'
)
return data


class SpecScanToMapConfigProcessor(Processor):
"""Processor to get the
Expand Down
19 changes: 19 additions & 0 deletions CHAP/common/models/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,25 @@ def validate_vrange(cls, vrange, info):
for i in info.data['index_range']]


class IndexSliceConfig(CHAPBaseModel):
"""Configuration for a python `sslice` object.

:ivar start: A `start` parameter for `slice()`, defaults to 0.
:vartype start: int, optional
:ivar stop: A `stop` parameter for `slice()`, defaults to -1.
:vartype stop: int, optional
:ivar step: A `step` parameter for `slice()`, defaults to 1.
:vartype step: int, optional
"""
start: Optional[int] = 0
stop: Optional[int] = -1
step: Optional[int] = 1

@property
def _slice(self):
return slice(self.start, self.stop, self.step)


class UnstructuredToStructuredConfig(CHAPBaseModel):
"""Configuration class to reshape data in an
`NXdata <https://manual.nexusformat.org/classes/base_classes/NXdata.html>`__
Expand Down
Loading