From 74feee28f628dc33ba07f62d94e2fd0d82290fc9 Mon Sep 17 00:00:00 2001 From: YooSunYoung <17974113+YooSunYoung@users.noreply.github.com> Date: Mon, 27 Apr 2026 16:47:08 +0200 Subject: [PATCH 1/4] Allow setting output time bin unit. --- packages/essnmx/src/ess/nmx/configurations.py | 9 ++++++++ packages/essnmx/src/ess/nmx/executables.py | 15 ++++++++----- packages/essnmx/tests/executable_test.py | 22 +++++++++++++------ 3 files changed, 34 insertions(+), 12 deletions(-) diff --git a/packages/essnmx/src/ess/nmx/configurations.py b/packages/essnmx/src/ess/nmx/configurations.py index 3a4209885..a5972f6c9 100644 --- a/packages/essnmx/src/ess/nmx/configurations.py +++ b/packages/essnmx/src/ess/nmx/configurations.py @@ -203,6 +203,15 @@ class OutputConfig(BaseModel): description="Compress option of reduced output file.", default=Compression.BITSHUFFLE_LZ4, ) + output_time_bin_unit: TimeBinUnit = Field( + title="Output Time Bin Unit", + description="Time bin unit in the output file. " + "If the input time bin is different from the output time bin unit, " + "the unit will be converted to the output time bin " + "before it is written in the output file.", + default=TimeBinUnit.ns, + # DIALS expects [ns] by default. + ) class ReductionConfig(BaseModel): diff --git a/packages/essnmx/src/ess/nmx/executables.py b/packages/essnmx/src/ess/nmx/executables.py index 8d3f26173..b3d98e259 100644 --- a/packages/essnmx/src/ess/nmx/executables.py +++ b/packages/essnmx/src/ess/nmx/executables.py @@ -186,7 +186,7 @@ def _build_time_bin_edges( true_last_bin_edge = bin_edges[t_coord_name, -1] + time_bin_width bin_edges = sc.concat([bin_edges, true_last_bin_edge], dim=t_coord_name) - return bin_edges + return bin_edges.to(dtype=float) else: # Number of bin edges are given but not the bin width. n_edges = wf_config.nbins + 1 @@ -197,7 +197,9 @@ def _build_time_bin_edges( # Avoid dropping the event that has the exact same # `event_time_offset`` or `tof` value as the upper bin edge. max_t.value = np.nextafter(max_t.value, np.inf) - return sc.linspace(dim=t_coord_name, start=min_t, stop=max_t, num=n_edges) + return sc.linspace( + dim=t_coord_name, start=min_t, stop=max_t, num=n_edges, dtype=float + ) def reduction( @@ -281,11 +283,14 @@ def reduction( ) # Histogram detector counts + output_tunit = config.output.output_time_bin_unit + t_bin_edges = t_bin_edges.to(unit=output_tunit) tof_histograms = sc.DataGroup() for detector_name, tof_da in tof_das.items(): - t_coord_unit = tof_da.bins.coords[t_coord_name].unit - histogram = tof_da.hist({t_coord_name: t_bin_edges.to(unit=t_coord_unit)}) - tof_histograms[detector_name] = histogram + tof_da.bins.coords[t_coord_name] = tof_da.bins.coords[t_coord_name].to( + unit=output_tunit + ) + tof_histograms[detector_name] = tof_da.hist({t_coord_name: t_bin_edges}) _tof_histogram = next(iter(tof_histograms.values())) monitor_metadata = NMXMonitorMetadata( diff --git a/packages/essnmx/tests/executable_test.py b/packages/essnmx/tests/executable_test.py index fa8ed8621..13e0b6411 100644 --- a/packages/essnmx/tests/executable_test.py +++ b/packages/essnmx/tests/executable_test.py @@ -108,6 +108,7 @@ def test_reduction_config() -> None: verbose=True, skip_file_output=True, overwrite=True, + output_time_bin_unit=TimeBinUnit.us, ) expected_config = ReductionConfig( inputs=input_options, workflow=workflow_options, output=output_options @@ -162,9 +163,11 @@ def _check_output_file( if nbins and bin_width is None: assert len(toa_edges) == nbins else: - assert (toa_edges[1] - toa_edges[0]) == sc.scalar( - bin_width, unit='ms' - ).to(unit='us') + computed_bin_width = toa_edges[1] - toa_edges[0] + expected_bin_width = sc.scalar(bin_width, unit='ms').to( + unit=computed_bin_width.unit + ) + assert_identical(computed_bin_width, expected_bin_width) assert all(field_name in det_gr for field_name in mandatory_fields) @@ -173,14 +176,14 @@ def test_executable_runs(small_nmx_nexus_path, tmp_path: pathlib.Path): output_file = tmp_path / "output.h5" assert not output_file.exists() - bin_width = 10 # Bigger bins for testing. + bin_width = 10.0 # Bigger bins for testing. # The output has 1280x1280 pixels per detector per time bin. commands = ( 'essnmx-reduce', '--input-file', small_nmx_nexus_path, '--time-bin-width', - str(bin_width), + str(int(bin_width)), '--output-file', output_file.as_posix(), ) @@ -294,7 +297,9 @@ def test_reduction_only_time_bin_width(reduction_config: ReductionConfig) -> Non hist = _retrieve_one_hist(reduction(config=reduction_config)) width = hist.coords['tof'][1] - hist.coords['tof'][0] - assert width == sc.scalar(20.0, unit='ms').to(unit='us') + assert width == sc.scalar(20.0, unit='ms').to( + unit=reduction_config.output.output_time_bin_unit + ) def test_reduction_only_number_of_time_bins(reduction_config: ReductionConfig) -> None: @@ -315,7 +320,10 @@ def test_histogram_event_time_offset(reduction_config: ReductionConfig) -> None: # Check that the number of time bins is as expected. width = hist.coords['event_time_offset'][1] - hist.coords['event_time_offset'][0] - assert_identical(width, sc.scalar(20.0, unit='ms').to(unit='ns')) + expected_output_width = sc.scalar(20.0, unit='ms').to( + unit=reduction_config.output.output_time_bin_unit + ) + assert_identical(width, expected_output_width) # Check if the histogram result is reasonable zero = sc.scalar(0.0, unit='counts', dtype='float32', variance=0.0) assert bool(hist.data.sum() > zero) From 8805d2e47f6c877ae3f90fca7daf1d3dadeb11b4 Mon Sep 17 00:00:00 2001 From: YooSunYoung <17974113+YooSunYoung@users.noreply.github.com> Date: Mon, 27 Apr 2026 17:03:21 +0200 Subject: [PATCH 2/4] Result time bin unit should be in the workflow configuration. --- packages/essnmx/src/ess/nmx/configurations.py | 18 +++++++++--------- packages/essnmx/src/ess/nmx/executables.py | 3 +-- packages/essnmx/tests/executable_test.py | 6 +++--- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/packages/essnmx/src/ess/nmx/configurations.py b/packages/essnmx/src/ess/nmx/configurations.py index a5972f6c9..567845a64 100644 --- a/packages/essnmx/src/ess/nmx/configurations.py +++ b/packages/essnmx/src/ess/nmx/configurations.py @@ -170,6 +170,15 @@ def positive_nbins(self): description="Random seed for TOF simulation.", default=42, # No reason. ) + result_time_bin_unit: TimeBinUnit = Field( + title="Output Time Bin Unit", + description="Time bin unit of the histogram after reduction. " + "If the input time bin is different from the result time bin unit, " + "the unit will be converted to the result time bin " + "before the result is returned.", + default=TimeBinUnit.ns, + # DIALS expects [ns] by default. + ) class OutputConfig(BaseModel): @@ -203,15 +212,6 @@ class OutputConfig(BaseModel): description="Compress option of reduced output file.", default=Compression.BITSHUFFLE_LZ4, ) - output_time_bin_unit: TimeBinUnit = Field( - title="Output Time Bin Unit", - description="Time bin unit in the output file. " - "If the input time bin is different from the output time bin unit, " - "the unit will be converted to the output time bin " - "before it is written in the output file.", - default=TimeBinUnit.ns, - # DIALS expects [ns] by default. - ) class ReductionConfig(BaseModel): diff --git a/packages/essnmx/src/ess/nmx/executables.py b/packages/essnmx/src/ess/nmx/executables.py index b3d98e259..f659f9f25 100644 --- a/packages/essnmx/src/ess/nmx/executables.py +++ b/packages/essnmx/src/ess/nmx/executables.py @@ -283,7 +283,7 @@ def reduction( ) # Histogram detector counts - output_tunit = config.output.output_time_bin_unit + output_tunit = config.workflow.result_time_bin_unit t_bin_edges = t_bin_edges.to(unit=output_tunit) tof_histograms = sc.DataGroup() for detector_name, tof_da in tof_das.items(): @@ -330,7 +330,6 @@ def reduction( def save_results(*, results: NMXLauetof, output_config: OutputConfig) -> None: # Validate if results have expected fields - export_static_metadata_as_nxlauetof( sample_metadata=results.sample, source_metadata=results.instrument.source, diff --git a/packages/essnmx/tests/executable_test.py b/packages/essnmx/tests/executable_test.py index 13e0b6411..7997bf886 100644 --- a/packages/essnmx/tests/executable_test.py +++ b/packages/essnmx/tests/executable_test.py @@ -101,6 +101,7 @@ def test_reduction_config() -> None: tof_simulation_min_ltotal=140.0, tof_simulation_max_ltotal=200.0, tof_simulation_seed=12345, + result_time_bin_unit=TimeBinUnit.us, ) output_options = OutputConfig( output_file='test-output.h5', @@ -108,7 +109,6 @@ def test_reduction_config() -> None: verbose=True, skip_file_output=True, overwrite=True, - output_time_bin_unit=TimeBinUnit.us, ) expected_config = ReductionConfig( inputs=input_options, workflow=workflow_options, output=output_options @@ -298,7 +298,7 @@ def test_reduction_only_time_bin_width(reduction_config: ReductionConfig) -> Non width = hist.coords['tof'][1] - hist.coords['tof'][0] assert width == sc.scalar(20.0, unit='ms').to( - unit=reduction_config.output.output_time_bin_unit + unit=reduction_config.workflow.result_time_bin_unit ) @@ -321,7 +321,7 @@ def test_histogram_event_time_offset(reduction_config: ReductionConfig) -> None: # Check that the number of time bins is as expected. width = hist.coords['event_time_offset'][1] - hist.coords['event_time_offset'][0] expected_output_width = sc.scalar(20.0, unit='ms').to( - unit=reduction_config.output.output_time_bin_unit + unit=reduction_config.workflow.result_time_bin_unit ) assert_identical(width, expected_output_width) # Check if the histogram result is reasonable From 6e82a1f1cea22f6bf91882acc6d2ffd6aece1ab9 Mon Sep 17 00:00:00 2001 From: Sunyoung Yoo <17974113+YooSunYoung@users.noreply.github.com> Date: Mon, 11 May 2026 16:11:02 +0200 Subject: [PATCH 3/4] Dtype to float to not lose precision. --- packages/essnmx/src/ess/nmx/executables.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/essnmx/src/ess/nmx/executables.py b/packages/essnmx/src/ess/nmx/executables.py index f659f9f25..2e4c81f47 100644 --- a/packages/essnmx/src/ess/nmx/executables.py +++ b/packages/essnmx/src/ess/nmx/executables.py @@ -288,6 +288,7 @@ def reduction( tof_histograms = sc.DataGroup() for detector_name, tof_da in tof_das.items(): tof_da.bins.coords[t_coord_name] = tof_da.bins.coords[t_coord_name].to( + dtype=float, unit=output_tunit ) tof_histograms[detector_name] = tof_da.hist({t_coord_name: t_bin_edges}) From e473b02955785ba14a26a1eaad59c181269c84b9 Mon Sep 17 00:00:00 2001 From: Sunyoung Yoo <17974113+YooSunYoung@users.noreply.github.com> Date: Mon, 11 May 2026 16:14:09 +0200 Subject: [PATCH 4/4] Make ruff hapy --- packages/essnmx/src/ess/nmx/executables.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/essnmx/src/ess/nmx/executables.py b/packages/essnmx/src/ess/nmx/executables.py index 2e4c81f47..48e7e6457 100644 --- a/packages/essnmx/src/ess/nmx/executables.py +++ b/packages/essnmx/src/ess/nmx/executables.py @@ -288,8 +288,7 @@ def reduction( tof_histograms = sc.DataGroup() for detector_name, tof_da in tof_das.items(): tof_da.bins.coords[t_coord_name] = tof_da.bins.coords[t_coord_name].to( - dtype=float, - unit=output_tunit + dtype=float, unit=output_tunit ) tof_histograms[detector_name] = tof_da.hist({t_coord_name: t_bin_edges})