Skip to content

Commit d4cebb8

Browse files
committed
Add documentation of reduction interface.
1 parent 03941fb commit d4cebb8

File tree

7 files changed

+279
-67
lines changed

7 files changed

+279
-67
lines changed

docs/user-guide/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
maxdepth: 1
66
---
77
8+
workflow
89
mcstas_workflow
910
mcstas_workflow_chunk
1011
scaling_workflow

docs/user-guide/workflow.ipynb

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"# NMX Reduction Workflow\n",
8+
"\n",
9+
"> NMX does not expect users to use python interface directly.<br>\n",
10+
"This documentation is mostly for istrument data scientists or instrument scientists.<br>"
11+
]
12+
},
13+
{
14+
"cell_type": "markdown",
15+
"metadata": {},
16+
"source": [
17+
"## TL;DR"
18+
]
19+
},
20+
{
21+
"cell_type": "code",
22+
"execution_count": null,
23+
"metadata": {},
24+
"outputs": [],
25+
"source": [
26+
"from ess.nmx.executables import reduction\n",
27+
"from ess.nmx.data import get_small_nmx_nexus\n",
28+
"from ess.nmx.configurations import (\n",
29+
" ReductionConfig,\n",
30+
" OutputConfig,\n",
31+
" InputConfig,\n",
32+
" WorkflowConfig,\n",
33+
" TimeBinCoordinate,\n",
34+
")\n",
35+
"\n",
36+
"# Build Configuration\n",
37+
"config = ReductionConfig(\n",
38+
" inputs=InputConfig(\n",
39+
" input_file=[get_small_nmx_nexus().as_posix()],\n",
40+
" detector_ids=[0, 1, 2], # Detector index to be reduced in alphabetical order.\n",
41+
" ),\n",
42+
" output=OutputConfig(\n",
43+
" output_file=\"scipp_output.hdf\", skip_file_output=False, overwrite=True\n",
44+
" ),\n",
45+
" workflow=WorkflowConfig(\n",
46+
" time_bin_coordinate=TimeBinCoordinate.time_of_flight,\n",
47+
" nbins=10,\n",
48+
" tof_simulation_num_neutrons=1_000_000,\n",
49+
" tof_simulation_min_wavelength=1.8,\n",
50+
" tof_simulation_max_wavelength=3.6,\n",
51+
" tof_simulation_seed=42,\n",
52+
" ),\n",
53+
")\n",
54+
"\n",
55+
"# Run Reduction\n",
56+
"reduction(config=config, display=display)"
57+
]
58+
},
59+
{
60+
"cell_type": "markdown",
61+
"metadata": {},
62+
"source": [
63+
"## Configuration\n",
64+
"\n",
65+
"`essnmx` provide command line data reduction tool for the reduction between `nexus` and `dials`.<br>\n",
66+
"The `essnmx-reduce` interface will reduce `nexus` file <br>\n",
67+
"and save the results into `NXlauetof`(not exactly but very close) format for `dials`.<br>\n",
68+
"\n",
69+
"Argument options could be exhaustive therefore we wrapped them into a nested pydantic model.<br>\n",
70+
"Here is a python API you can use to build the configuration and turn it into a command line arguments.\n",
71+
"\n",
72+
"**Configuration object is pydantic model so it strictly check the type of the arguments.**"
73+
]
74+
},
75+
{
76+
"cell_type": "code",
77+
"execution_count": null,
78+
"metadata": {},
79+
"outputs": [],
80+
"source": [
81+
"from ess.nmx.configurations import (\n",
82+
" ReductionConfig,\n",
83+
" OutputConfig,\n",
84+
" InputConfig,\n",
85+
" WorkflowConfig,\n",
86+
" TimeBinCoordinate,\n",
87+
" to_command_arguments,\n",
88+
")\n",
89+
"\n",
90+
"config = ReductionConfig(\n",
91+
" inputs=InputConfig(\n",
92+
" input_file=[\"PATH_TO_THE_NEXUS_FILE.hdf\"],\n",
93+
" detector_ids=[0, 1, 2], # Detector index to be reduced in alphabetical order.\n",
94+
" ),\n",
95+
" output=OutputConfig(output_file=\"scipp_output.hdf\", skip_file_output=True),\n",
96+
" workflow=WorkflowConfig(\n",
97+
" time_bin_coordinate=TimeBinCoordinate.time_of_flight,\n",
98+
" nbins=10,\n",
99+
" tof_simulation_num_neutrons=1_000_000,\n",
100+
" tof_simulation_min_wavelength=1.8,\n",
101+
" tof_simulation_max_wavelength=3.6,\n",
102+
" tof_simulation_seed=42,\n",
103+
" ),\n",
104+
")\n",
105+
"\n",
106+
"display(config)\n",
107+
"print(to_command_arguments(config=config, one_line=True))"
108+
]
109+
},
110+
{
111+
"cell_type": "markdown",
112+
"metadata": {},
113+
"source": [
114+
"## Reduce Nexus File(s)\n",
115+
"\n",
116+
"`OutputConfig` has an option called `skip_file_output` if you want to reduce the file and use it only on the memory.<br>\n",
117+
"Then you can use `save_results` function to explicitly save the results."
118+
]
119+
},
120+
{
121+
"cell_type": "code",
122+
"execution_count": null,
123+
"metadata": {},
124+
"outputs": [],
125+
"source": [
126+
"from ess.nmx.executables import reduction\n",
127+
"from ess.nmx.data import get_small_nmx_nexus\n",
128+
"\n",
129+
"config = ReductionConfig(\n",
130+
" inputs=InputConfig(input_file=[get_small_nmx_nexus().as_posix()]),\n",
131+
" output=OutputConfig(skip_file_output=True),\n",
132+
")\n",
133+
"results = reduction(config=config, display=display)\n",
134+
"results"
135+
]
136+
},
137+
{
138+
"cell_type": "code",
139+
"execution_count": null,
140+
"metadata": {},
141+
"outputs": [],
142+
"source": [
143+
"from ess.nmx.executables import save_results\n",
144+
"\n",
145+
"output_config = OutputConfig(output_file=\"scipp_output.hdf\", overwrite=True)\n",
146+
"save_results(results=results, output_config=output_config)"
147+
]
148+
}
149+
],
150+
"metadata": {
151+
"kernelspec": {
152+
"display_name": "nmx-dev-313",
153+
"language": "python",
154+
"name": "python3"
155+
},
156+
"language_info": {
157+
"codemirror_mode": {
158+
"name": "ipython",
159+
"version": 3
160+
},
161+
"file_extension": ".py",
162+
"mimetype": "text/x-python",
163+
"name": "python",
164+
"nbconvert_exporter": "python",
165+
"pygments_lexer": "ipython3",
166+
"version": "3.13.5"
167+
}
168+
},
169+
"nbformat": 4,
170+
"nbformat_minor": 4
171+
}

src/ess/nmx/_executable_helper.py

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -174,42 +174,6 @@ def reduction_config_from_args(args: argparse.Namespace) -> ReductionConfig:
174174
)
175175

176176

177-
def to_command_arguments(
178-
config: ReductionConfig, one_line: bool = True
179-
) -> list[str] | str:
180-
"""Convert the config to a list of command line arguments.
181-
182-
Parameters
183-
----------
184-
one_line:
185-
If True, return a single string with all arguments joined by spaces.
186-
If False, return a list of argument strings.
187-
188-
"""
189-
args = {}
190-
for instance in config._children:
191-
args.update(instance.model_dump(mode='python'))
192-
args = {f"--{k.replace('_', '-')}": v for k, v in args.items() if v is not None}
193-
194-
arg_list = []
195-
for k, v in args.items():
196-
if not isinstance(v, bool):
197-
arg_list.append(k)
198-
if isinstance(v, list):
199-
arg_list.extend(str(item) for item in v)
200-
elif isinstance(v, enum.StrEnum):
201-
arg_list.append(v.value)
202-
else:
203-
arg_list.append(str(v))
204-
elif v is True:
205-
arg_list.append(k)
206-
207-
if one_line:
208-
return ' '.join(arg_list)
209-
else:
210-
return arg_list
211-
212-
213177
def build_logger(args: argparse.Namespace | OutputConfig) -> logging.Logger:
214178
logger = logging.getLogger(__name__)
215179
if args.verbose:

src/ess/nmx/configurations.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,11 +130,22 @@ class OutputConfig(BaseModel):
130130
default=False,
131131
)
132132
# File output
133+
skip_file_output: bool = Field(
134+
title="Skip File Output",
135+
description="If True, the output file will not be written.",
136+
default=False,
137+
)
133138
output_file: str = Field(
134139
title="Output File",
135-
description="Path to the output file.",
140+
description="Path to the output file. "
141+
"It will be overwritten if ``overwrite`` is True.",
136142
default="scipp_output.h5",
137143
)
144+
overwrite: bool = Field(
145+
title="Overwrite Output File",
146+
description="If True, overwrite the output file if ``output_file`` exists.",
147+
default=False,
148+
)
138149
compression: Compression = Field(
139150
title="Compression",
140151
description="Compress option of reduced output file.",
@@ -152,3 +163,45 @@ class ReductionConfig(BaseModel):
152163
@property
153164
def _children(self) -> list[BaseModel]:
154165
return [self.inputs, self.workflow, self.output]
166+
167+
168+
def to_command_arguments(
169+
*, config: ReductionConfig, one_line: bool = True, separator: str = '\\\n'
170+
) -> list[str] | str:
171+
"""Convert the config to a list of command line arguments.
172+
173+
Parameters
174+
----------
175+
one_line:
176+
If True, return a single string with all arguments joined by spaces.
177+
If False, return a list of argument strings.
178+
179+
"""
180+
args = {}
181+
for instance in config._children:
182+
args.update(instance.model_dump(mode='python'))
183+
args = {f"--{k.replace('_', '-')}": v for k, v in args.items() if v is not None}
184+
185+
arg_list = []
186+
for k, v in args.items():
187+
if not isinstance(v, bool):
188+
arg_list.append(k)
189+
if isinstance(v, list):
190+
arg_list.extend(str(item) for item in v)
191+
elif isinstance(v, enum.StrEnum):
192+
arg_list.append(v.value)
193+
else:
194+
arg_list.append(str(v))
195+
elif v is True:
196+
arg_list.append(k)
197+
198+
if one_line:
199+
# Default separator is backslash + newline for better readability
200+
# Users can directly copy-paste the output in a terminal or a script.
201+
return (
202+
(separator + '--')
203+
.join(" ".join(arg_list).split('--'))
204+
.removeprefix(separator)
205+
)
206+
else:
207+
return arg_list

src/ess/nmx/executables.py

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
collect_matching_input_files,
1818
reduction_config_from_args,
1919
)
20-
from .configurations import ReductionConfig, WorkflowConfig
20+
from .configurations import OutputConfig, ReductionConfig, WorkflowConfig
2121
from .nexus import (
2222
export_detector_metadata_as_nxlauetof,
2323
export_monitor_metadata_as_nxlauetof,
@@ -160,27 +160,17 @@ def reduction(
160160
display=display,
161161
)
162162
metadatas = base_wf.compute((NMXSampleMetadata, NMXSourceMetadata))
163-
export_static_metadata_as_nxlauetof(
164-
sample_metadata=metadatas[NMXSampleMetadata],
165-
source_metadata=metadatas[NMXSourceMetadata],
166-
output_file=config.output.output_file,
167-
)
168163
tof_das = sc.DataGroup()
169164
detector_metas = sc.DataGroup()
170165
for detector_name in detector_names:
171166
cur_wf = base_wf.copy()
172167
cur_wf[NeXusName[snx.NXdetector]] = detector_name
173168
results = cur_wf.compute((TofDetector[SampleRun], NMXDetectorMetadata))
174-
detector_meta: NMXDetectorMetadata = results[NMXDetectorMetadata]
175-
export_detector_metadata_as_nxlauetof(
176-
detector_metadata=detector_meta, output_file=config.output.output_file
177-
)
178-
detector_metas[detector_name] = detector_meta
169+
detector_metas[detector_name] = results[NMXDetectorMetadata]
179170
# Binning into 1 bin and getting final tof bin edges later.
180171
tof_das[detector_name] = results[TofDetector[SampleRun]].bin(tof=1)
181172

182173
tof_bin_edges = _finalize_tof_bin_edges(tof_das=tof_das, config=config.workflow)
183-
184174
monitor_metadata = NMXMonitorMetadata(
185175
tof_bin_coord='tof',
186176
# TODO: Use real monitor data
@@ -190,28 +180,54 @@ def reduction(
190180
data=sc.ones_like(tof_bin_edges[:-1]),
191181
),
192182
)
193-
export_monitor_metadata_as_nxlauetof(
194-
monitor_metadata=monitor_metadata, output_file=config.output.output_file
195-
)
196183

197184
# Histogram detector counts
198185
tof_histograms = sc.DataGroup()
199186
for detector_name, tof_da in tof_das.items():
200-
det_meta: NMXDetectorMetadata = detector_metas[detector_name]
201187
histogram = tof_da.hist(tof=tof_bin_edges)
202188
tof_histograms[detector_name] = histogram
203-
export_reduced_data_as_nxlauetof(
204-
detector_name=det_meta.detector_name,
205-
da=histogram,
206-
output_file=config.output.output_file,
207-
compress_mode=config.output.compression,
208-
)
209189

210-
return sc.DataGroup(
211-
metadata=detector_metas,
190+
results = sc.DataGroup(
212191
histogram=tof_histograms,
192+
detector=detector_metas,
193+
sample=metadatas[NMXSampleMetadata],
194+
source=metadatas[NMXSourceMetadata],
195+
monitor=monitor_metadata,
213196
lookup_table=base_wf.compute(TimeOfFlightLookupTable),
214197
)
198+
if not config.output.skip_file_output:
199+
save_results(results=results, output_config=config.output)
200+
201+
return results
202+
203+
204+
def save_results(*, results: sc.DataGroup, output_config: OutputConfig) -> None:
205+
# Validate if results have expected fields
206+
for mandatory_key in ['histogram', 'detector', 'sample', 'source', 'monitor']:
207+
if mandatory_key not in results:
208+
raise ValueError(f"Missing '{mandatory_key}' in results to save.")
209+
210+
export_static_metadata_as_nxlauetof(
211+
sample_metadata=results['sample'],
212+
source_metadata=results['source'],
213+
output_file=output_config.output_file,
214+
overwrite=output_config.overwrite,
215+
)
216+
export_monitor_metadata_as_nxlauetof(
217+
monitor_metadata=results['monitor'],
218+
output_file=output_config.output_file,
219+
)
220+
for detector_name, detector_meta in results['detector'].items():
221+
export_detector_metadata_as_nxlauetof(
222+
detector_metadata=detector_meta,
223+
output_file=output_config.output_file,
224+
)
225+
export_reduced_data_as_nxlauetof(
226+
detector_name=detector_name,
227+
da=results['histogram'][detector_name],
228+
output_file=output_config.output_file,
229+
compress_mode=output_config.compression,
230+
)
215231

216232

217233
def main() -> None:

0 commit comments

Comments
 (0)