diff --git a/src/ess/livedata/workflows/detector_view/providers.py b/src/ess/livedata/workflows/detector_view/providers.py index 2daac1d98..4fdb1fa3e 100644 --- a/src/ess/livedata/workflows/detector_view/providers.py +++ b/src/ess/livedata/workflows/detector_view/providers.py @@ -261,7 +261,7 @@ def detector_image( Returns ------- : - 2D detector image. + 2D detector image, published as float32. """ spectral_dim = histogram.dims[-1] if histogram_slice is not None: @@ -273,7 +273,12 @@ def detector_image( if use_weighting: image = image / weights - return DetectorImage[AccumulationMode](image) + # da00 is uncompressed, so float32 halves the wire size of every image (and what + # the dashboard buffers per frame). Only the published image is cast: the + # accumulation above must stay float64 because float32 stops incrementing at + # 2**24, which a long-running cumulative image can reach. Casting the result + # instead rounds it by at most one part in 2**24 and nothing accumulates on top. + return DetectorImage[AccumulationMode](image.to(dtype='float32', copy=False)) def counts_total( diff --git a/tests/config/tbl_specs_test.py b/tests/config/tbl_specs_test.py index 538c9d7e6..230a8b5ce 100644 --- a/tests/config/tbl_specs_test.py +++ b/tests/config/tbl_specs_test.py @@ -177,7 +177,8 @@ def test_multiblade_spectrum_sums_strips(): assert spectrum.sizes['blade'] == MULTIBLADE_SIZES['blade'] assert spectrum.sizes['wire'] == MULTIBLADE_SIZES['wire'] # Summing strips redistributes but does not lose counts. - assert sc.isclose(spectrum.sum().data, result['cumulative'].sum().data).value + # 'cumulative' is published as float32; compare values, not scipp variables. + assert spectrum.sum().value == pytest.approx(result['cumulative'].sum().value) def test_he3_spectrum_keeps_every_pixel(): @@ -195,7 +196,8 @@ def test_he3_spectrum_keeps_every_pixel(): assert spectrum.dims == ('tube', 'pixel', 'time_of_arrival') assert spectrum.sizes['tube'] == HE3_SIZES['dim_0'] assert spectrum.sizes['pixel'] == HE3_SIZES['dim_1'] - assert sc.isclose(spectrum.sum().data, result['cumulative'].sum().data).value + # 'cumulative' is published as float32; compare values, not scipp variables. + assert spectrum.sum().value == pytest.approx(result['cumulative'].sum().value) def test_he3_spectrum_does_not_alias_the_accumulator_buffer(): diff --git a/tests/workflows/detector_view/integration_test.py b/tests/workflows/detector_view/integration_test.py index 4e9ee2930..a0e7418a7 100644 --- a/tests/workflows/detector_view/integration_test.py +++ b/tests/workflows/detector_view/integration_test.py @@ -116,6 +116,12 @@ def test_full_workflow_accumulate_and_finalize(self): assert result['cumulative'].dims == ('y', 'x') assert result['cumulative'].sizes == {'y': 4, 'x': 4} + # Images go on the wire as float32; the count scalars are not images and + # keep the accumulator's dtype. + assert result['cumulative'].dtype == 'float32' + assert result['current'].dtype == 'float32' + assert result['counts_total_cumulative'].dtype == 'float64' + def test_cumulative_accumulates_current_resets(self): """Test that cumulative accumulates and current resets after finalize.""" # Use factory to create workflow (same code path as production) diff --git a/tests/workflows/detector_view/providers_test.py b/tests/workflows/detector_view/providers_test.py index 861dc139c..e4aee27fa 100644 --- a/tests/workflows/detector_view/providers_test.py +++ b/tests/workflows/detector_view/providers_test.py @@ -301,7 +301,9 @@ def test_detector_image_sums_over_spectral_dim(self): assert result.dims == ('y', 'x') assert result.sizes == {'y': 4, 'x': 4} # Each pixel should have sum of 10 spectral bins - expected = sc.full(dims=['y', 'x'], shape=[4, 4], value=10.0, unit='counts') + expected = sc.full( + dims=['y', 'x'], shape=[4, 4], value=10.0, unit='counts', dtype='float32' + ) assert sc.allclose(result.data, expected) def test_detector_image_with_histogram_slice(self): @@ -329,6 +331,57 @@ def test_detector_image_with_histogram_slice(self): # Should only sum ~5 bins (0-50000 ns from 0-100000 ns range) assert result.dims == ('y', 'x') + @pytest.mark.parametrize("use_weighting", [False, True]) + def test_detector_image_is_published_as_float32(self, use_weighting): + """The image is cast on the way out to halve the da00 payload.""" + data = sc.DataArray( + sc.ones( + dims=['y', 'x', 'event_time_offset'], + shape=[4, 4, 10], + unit='counts', + dtype='float64', + ) + ) + weights = PixelWeights(sc.full(dims=['y', 'x'], shape=[4, 4], value=2.0)) + + result = detector_image( + histogram=AccumulatedHistogram[Cumulative](data), + histogram_slice=None, + weights=weights, + use_weighting=UsePixelWeighting(use_weighting), + ) + + assert result.dtype == 'float32' + expected = 5.0 if use_weighting else 10.0 + assert sc.allclose( + result.data, + sc.full(dims=['y', 'x'], shape=[4, 4], value=expected, unit='counts').to( + dtype='float32' + ), + ) + + def test_detector_image_cast_leaves_accumulated_histogram_untouched(self): + """Accumulation must stay float64: float32 stops counting at 2**24.""" + data = sc.DataArray( + sc.full( + dims=['y', 'x', 'event_time_offset'], + shape=[1, 1, 1], + value=2.0**24, + unit='counts', + ) + ) + histogram = AccumulatedHistogram[Cumulative](data) + + detector_image( + histogram=histogram, + histogram_slice=None, + weights=PixelWeights(sc.ones(dims=['y', 'x'], shape=[1, 1])), + use_weighting=UsePixelWeighting(False), + ) + + assert histogram.dtype == 'float64' + assert (histogram + sc.scalar(1.0, unit='counts')).sum().value == 2.0**24 + 1 + class TestCountProviders: """Tests for count provider functions.""" diff --git a/tests/workflows/detector_view/spectrum_view_test.py b/tests/workflows/detector_view/spectrum_view_test.py index 665ba81d5..1e1b8698b 100644 --- a/tests/workflows/detector_view/spectrum_view_test.py +++ b/tests/workflows/detector_view/spectrum_view_test.py @@ -3,6 +3,7 @@ """Integration tests for the unified spectrum-view output.""" import pydantic +import pytest import scipp as sc from ess.reduce.nexus.types import RawDetector, SampleRun @@ -76,7 +77,8 @@ def test_spectrum_view_sums_over_declared_dim(self): assert spectrum.dims == ('x', 'time_of_arrival') cumulative = result['cumulative'] # Cumulative is summed over time_of_arrival => total counts match total events. - assert sc.isclose(spectrum.sum().data, cumulative.sum().data).value + # It is published as float32, so compare values rather than scipp variables. + assert spectrum.sum().value == pytest.approx(cumulative.sum().value) def test_spectrum_view_rebin_factor_applied(self): spec = SpectrumViewSpec(