From 794bf741638b9ebfc3efde2d287b7b74602ce544 Mon Sep 17 00:00:00 2001 From: esmithExperimentControls Date: Fri, 15 Nov 2024 16:58:30 +0000 Subject: [PATCH 01/25] Some tests for tof bounding of spectra --- tests/devices/simpledae/test_reducers.py | 125 ++++++++++++++++++++++- 1 file changed, 122 insertions(+), 3 deletions(-) diff --git a/tests/devices/simpledae/test_reducers.py b/tests/devices/simpledae/test_reducers.py index de784797..c4f89d5e 100644 --- a/tests/devices/simpledae/test_reducers.py +++ b/tests/devices/simpledae/test_reducers.py @@ -10,7 +10,9 @@ GoodFramesNormalizer, MonitorNormalizer, PeriodGoodFramesNormalizer, + ScalarNormalizer ) +from ibex_bluesky_core.devices.simpledae.reducers import tof_bounded_spectra @pytest.fixture @@ -34,6 +36,38 @@ async def monitor_normalizer() -> MonitorNormalizer: return reducer +@pytest.fixture +async def scalar_normalizer_bounded_sum_zero_to_one_half(simpledae: SimpleDae) -> ScalarNormalizer: + set_mock_value(simpledae.period.good_frames, 1) + reducer = PeriodGoodFramesNormalizer(prefix="", detector_spectra=[1,2], summer=tof_bounded_spectra(sc.array(dims=["tof"], values=[0, 0.5], unit=sc.units.us))) + await reducer.connect(mock=True) + return reducer + + +@pytest.fixture +async def scalar_normalizer_bounded_sum_one_to_one(simpledae: SimpleDae) -> ScalarNormalizer: + set_mock_value(simpledae.period.good_frames, 1) + reducer = PeriodGoodFramesNormalizer(prefix="", detector_spectra=[1,2], summer=tof_bounded_spectra(sc.array(dims=["tof"], values=[1.0, 1.0], unit=sc.units.us))) + await reducer.connect(mock=True) + return reducer + + +@pytest.fixture +async def spectra_bins_easy_to_test() -> sc.DataArray: + return sc.DataArray( + data=sc.Variable(dims=["tof"], values=[1000.0, 2000.0, 3000.0, 2000.0, 1000.0], unit=sc.units.counts, dtype="float64"), + coords={"tof": sc.array(dims=["tof"], values=[0, 1, 2, 3, 4, 5], unit=sc.units.us, dtype="float64")}, + ) + + +@pytest.fixture +async def scalar_normalizer_unbounded_sum(simpledae: SimpleDae) -> ScalarNormalizer: + set_mock_value(simpledae.period.good_frames, 1) + reducer = PeriodGoodFramesNormalizer(prefix="", detector_spectra=[1,2], summer=tof_bounded_spectra(sc.array(dims=["tof"], values=[], unit=sc.units.us))) + await reducer.connect(mock=True) + return reducer + + class FakePeriod: def __init__(self): self.good_frames = object() @@ -134,7 +168,7 @@ async def test_period_good_frames_normalizer_uncertainties( variances=[4000.0, 5000.0, 6000.0], unit=sc.units.counts, ), - coords={"tof": sc.array(dims=["tof"], values=[0, 1, 2, 3])}, + coords={"tof": sc.array(dims=["tof"], values=[0, 1, 2, 3], unit=sc.units.us, dtype="float64")}, ) ) @@ -158,7 +192,7 @@ async def test_period_good_frames_normalizer_zero_counts( variances=[0.0, 0.0, 0.0], unit=sc.units.counts, ), - coords={"tof": sc.array(dims=["tof"], values=[0, 1, 2, 3])}, + coords={"tof": sc.array(dims=["tof"], values=[0, 1, 2, 3], unit=sc.units.us, dtype="float64")}, ) ) period_good_frames_reducer.detectors[2].read_spectrum_dataarray = AsyncMock( @@ -169,7 +203,7 @@ async def test_period_good_frames_normalizer_zero_counts( variances=[0.0, 0.0, 0.0], unit=sc.units.counts, ), - coords={"tof": sc.array(dims=["tof"], values=[0, 1, 2, 3])}, + coords={"tof": sc.array(dims=["tof"], values=[0, 1, 2, 3], unit=sc.units.us, dtype="float64")}, ) ) @@ -302,3 +336,88 @@ async def test_monitor_normalizer_publishes_raw_and_normalized_count_uncertainti assert monitor_normalizer.intensity_stddev in readables assert monitor_normalizer.det_counts_stddev in readables assert monitor_normalizer.mon_counts_stddev in readables + +async def test_tof_bounded_spectra_half_of_first_bin_rebinned_correctly( + simpledae: SimpleDae, + scalar_normalizer_bounded_sum_zero_to_one_half: ScalarNormalizer + ): + + scalar_normalizer_bounded_sum_zero_to_one_half.detectors[1].read_spectrum_dataarray = AsyncMock( + return_value=sc.DataArray( + data=sc.Variable( + dims=["tof"], + values=[1000.0, 2000.0, 3000.0], + variances=[1000.0, 2000.0, 3000.0], + unit=sc.units.counts, + dtype="float64" + ), + coords={"tof": sc.array(dims=["tof"], values=[0, 1, 2, 3], unit=sc.units.us, + dtype="float64")}, + ) + ) + scalar_normalizer_bounded_sum_zero_to_one_half.detectors[2].read_spectrum_dataarray = AsyncMock( + return_value=sc.DataArray( + data=sc.Variable( + dims=["tof"], + values=[4000.0, 5000.0, 6000.0], + variances=[4000.0, 5000.0, 6000.0], + unit=sc.units.counts, + dtype="float64", + ), + coords={"tof": sc.array(dims=["tof"], values=[0, 1, 2, 3], unit=sc.units.us, + dtype="float64")}, + ) + ) + + await scalar_normalizer_bounded_sum_zero_to_one_half.reduce_data(simpledae) + + det_counts = await scalar_normalizer_bounded_sum_zero_to_one_half.det_counts.get_value() + intensity = await scalar_normalizer_bounded_sum_zero_to_one_half.intensity.get_value() + + assert det_counts == 2500 # 500 from first detector + 2000 from second detector + assert intensity == pytest.approx(2500) + + +async def test_tof_bounded_spectra_upper_and_lower_bound_equal( + simpledae: SimpleDae, + scalar_normalizer_bounded_sum_one_to_one: ScalarNormalizer, + spectra_bins_easy_to_test: sc.DataArray +): + + scalar_normalizer_bounded_sum_one_to_one.detectors[1].read_spectrum_dataarray = AsyncMock( + return_value=spectra_bins_easy_to_test + ) + + scalar_normalizer_bounded_sum_one_to_one.detectors[2].read_spectrum_dataarray = AsyncMock( + return_value=spectra_bins_easy_to_test + ) + + await scalar_normalizer_bounded_sum_one_to_one.reduce_data(simpledae) + + det_counts = await scalar_normalizer_bounded_sum_one_to_one.det_counts.get_value() + intensity = await scalar_normalizer_bounded_sum_one_to_one.intensity.get_value() + + assert det_counts == 0 # 500 from first detector + 2000 from second detector + assert intensity == pytest.approx(0) + + +async def test_tof_bounded_spectra_no_upper_bound( + simpledae: SimpleDae, + scalar_normalizer_unbounded_sum: ScalarNormalizer, + spectra_bins_easy_to_test: sc.DataArray +): + scalar_normalizer_unbounded_sum.detectors[1].read_spectrum_dataarray = AsyncMock( + return_value=spectra_bins_easy_to_test + ) + + scalar_normalizer_unbounded_sum.detectors[2].read_spectrum_dataarray = AsyncMock( + return_value=spectra_bins_easy_to_test + ) + + await scalar_normalizer_unbounded_sum.reduce_data(simpledae) + + det_counts = await scalar_normalizer_unbounded_sum.det_counts.get_value() + intensity = await scalar_normalizer_unbounded_sum.intensity.get_value() + + assert det_counts == 9000.0 # 1 + 2 + 3 + 2 + 1 thousand = 9 thousand + assert intensity == pytest.approx(9000.0) \ No newline at end of file From e90c0a318a703c3f24d081562d6dec2b1beea138 Mon Sep 17 00:00:00 2001 From: esmithExperimentControls Date: Fri, 15 Nov 2024 16:58:58 +0000 Subject: [PATCH 02/25] Adding explicit declaration of data type --- src/ibex_bluesky_core/devices/dae/dae_spectra.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ibex_bluesky_core/devices/dae/dae_spectra.py b/src/ibex_bluesky_core/devices/dae/dae_spectra.py index 0c904691..901df1e1 100644 --- a/src/ibex_bluesky_core/devices/dae/dae_spectra.py +++ b/src/ibex_bluesky_core/devices/dae/dae_spectra.py @@ -108,6 +108,6 @@ async def read_spectrum_dataarray(self) -> sc.DataArray: raise ValueError("Could not determine engineering units of tof edges.") return sc.DataArray( - data=sc.Variable(dims=["tof"], values=counts, variances=counts, unit=sc.units.counts), - coords={"tof": sc.array(dims=["tof"], values=tof_edges, unit=sc.Unit(unit))}, + data=sc.Variable(dims=["tof"], values=counts, variances=counts, unit=sc.units.counts, dtype="float64"), + coords={"tof": sc.array(dims=["tof"], values=tof_edges, unit=sc.Unit(unit), dtype="float64")}, ) From 9cc4810ee45f5af6870bace9187e591cdf1b4241 Mon Sep 17 00:00:00 2001 From: esmithExperimentControls Date: Fri, 15 Nov 2024 16:59:27 +0000 Subject: [PATCH 03/25] Add new summation for tof bounding of spectra --- .../devices/simpledae/reducers.py | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/ibex_bluesky_core/devices/simpledae/reducers.py b/src/ibex_bluesky_core/devices/simpledae/reducers.py index 4abe0a86..1daf6a83 100644 --- a/src/ibex_bluesky_core/devices/simpledae/reducers.py +++ b/src/ibex_bluesky_core/devices/simpledae/reducers.py @@ -13,6 +13,7 @@ StandardReadable, soft_signal_r_and_setter, ) +from typing import Callable, Awaitable from ibex_bluesky_core.devices.dae.dae_spectra import DaeSpectra from ibex_bluesky_core.devices.simpledae.strategies import Reducer @@ -36,10 +37,29 @@ async def sum_spectra(spectra: Collection[DaeSpectra]) -> sc.Variable | sc.DataA return summed_counts +def tof_bounded_spectra(bounds: sc.Variable) -> Callable[[Collection[DaeSpectra]], Awaitable[sc.Variable | sc.DataArray]]: + if "tof" not in bounds.dims: + raise ValueError("Should contain tof dims") # todo write tests covering this + if bounds.sizes["tof"] != 2: + raise ValueError("Should contain lower and upper bound") # todo write tests covering this + async def sum_spectra_with_tof(spectra: Collection[DaeSpectra]) -> sc.Variable | sc.DataArray: + """ + sums spectra, bounded by a time of flight upper and lower bound + """ + summed_counts = sc.scalar(value=0, unit=sc.units.counts, dtype="float64") + for spec in asyncio.as_completed([s.read_spectrum_dataarray() for s in spectra]): + tof_bound_spectra = await spec + summed_counts += tof_bound_spectra.rebin({ + "tof": bounds + }).sum() + return summed_counts + return sum_spectra_with_tof + + class ScalarNormalizer(Reducer, StandardReadable, metaclass=ABCMeta): """Sum a set of user-specified spectra, then normalize by a scalar signal.""" - def __init__(self, prefix: str, detector_spectra: Sequence[int]) -> None: + def __init__(self, prefix: str, detector_spectra: Sequence[int], summer: Callable[[Collection[DaeSpectra]], Awaitable[sc.Variable | sc.DataArray]] = sum_spectra) -> None: """Init. Args: @@ -65,6 +85,7 @@ def __init__(self, prefix: str, detector_spectra: Sequence[int]) -> None: self.intensity_stddev, self._intensity_stddev_setter = soft_signal_r_and_setter( float, 0.0, precision=INTENSITY_PRECISION ) + self.summer = summer super().__init__(name="") @@ -75,7 +96,7 @@ def denominator(self, dae: "SimpleDae") -> SignalR[int] | SignalR[float]: async def reduce_data(self, dae: "SimpleDae") -> None: """Apply the normalization.""" summed_counts, denominator = await asyncio.gather( - sum_spectra(self.detectors.values()), self.denominator(dae).get_value() + self.summer(self.detectors.values()), self.denominator(dae).get_value() ) self._det_counts_setter(float(summed_counts.value)) From 10edc74dcddf4406db0161290df6943a74898aea Mon Sep 17 00:00:00 2001 From: esmithExperimentControls Date: Tue, 19 Nov 2024 14:02:53 +0000 Subject: [PATCH 04/25] Add tof_bounded_spectra functionality to monitor normalizer as well, default monitor and detector summers to sum_spectra --- src/ibex_bluesky_core/devices/simpledae/reducers.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/ibex_bluesky_core/devices/simpledae/reducers.py b/src/ibex_bluesky_core/devices/simpledae/reducers.py index 1daf6a83..191a63ca 100644 --- a/src/ibex_bluesky_core/devices/simpledae/reducers.py +++ b/src/ibex_bluesky_core/devices/simpledae/reducers.py @@ -145,7 +145,11 @@ class MonitorNormalizer(Reducer, StandardReadable): """Normalize a set of user-specified detector spectra by user-specified monitor spectra.""" def __init__( - self, prefix: str, detector_spectra: Sequence[int], monitor_spectra: Sequence[int] + self, prefix: str, detector_spectra: Sequence[int], monitor_spectra: Sequence[int], + detector_summer: Callable[[Collection[DaeSpectra]], + Awaitable[sc.Variable | sc.DataArray]] = sum_spectra, + monitor_summer: Callable[[Collection[DaeSpectra]], + Awaitable[sc.Variable | sc.DataArray]] = sum_spectra ) -> None: """Init. @@ -178,13 +182,15 @@ def __init__( self.intensity_stddev, self._intensity_stddev_setter = soft_signal_r_and_setter( float, 0.0, precision=INTENSITY_PRECISION ) + self.detector_summer = detector_summer + self.monitor_summer = monitor_summer super().__init__(name="") async def reduce_data(self, dae: "SimpleDae") -> None: """Apply the normalization.""" detector_counts, monitor_counts = await asyncio.gather( - sum_spectra(self.detectors.values()), sum_spectra(self.monitors.values()) + self.detector_summer(self.detectors.values()), self.monitor_summer(self.monitors.values()) ) if monitor_counts.value == 0.0: # To avoid zero division From 7f30bc1ee8b15e049fae70247064ef551f0c240d Mon Sep 17 00:00:00 2001 From: esmithExperimentControls Date: Thu, 21 Nov 2024 13:36:37 +0000 Subject: [PATCH 05/25] Add scippneutron to dependencies --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index f5b8a887..82786a68 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,6 +49,7 @@ dependencies = [ "numpy", "scipp", "tzdata", + "scippneutron", ] [project.optional-dependencies] From 2f386a5478eca731961f33874935ea7ada0addce Mon Sep 17 00:00:00 2001 From: esmithExperimentControls Date: Thu, 21 Nov 2024 13:39:32 +0000 Subject: [PATCH 06/25] Add wavelength_bounded_spectra method to convert from tof to wavelength --- .../devices/simpledae/reducers.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/ibex_bluesky_core/devices/simpledae/reducers.py b/src/ibex_bluesky_core/devices/simpledae/reducers.py index 191a63ca..fa91a00e 100644 --- a/src/ibex_bluesky_core/devices/simpledae/reducers.py +++ b/src/ibex_bluesky_core/devices/simpledae/reducers.py @@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Collection, Sequence import scipp as sc +from scippneutron import conversion from ophyd_async.core import ( Device, DeviceVector, @@ -56,6 +57,24 @@ async def sum_spectra_with_tof(spectra: Collection[DaeSpectra]) -> sc.Variable | return sum_spectra_with_tof +def wavelength_bounded_spectra(bounds: sc.Variable, beam_total: sc.Variable) -> Callable[[Collection[DaeSpectra]], Awaitable[sc.Variable | sc.DataArray]]: + if "tof" not in bounds.dims: + raise ValueError("Should contain tof dims") + + async def sum_spectra_with_wavelength(spectra: Collection[DaeSpectra]) -> sc.Variable | sc.DataArray: + """ + sums spectra, converting time of flight to wavelength + """ + summed_counts = sc.scalar(value=0, unit=sc.units.counts, dtype="float64") + for spec in asyncio.as_completed([s.read_spectrum_dataarray() for s in spectra]): + wavelength_bounded_spectra = await spec + wavelength_coord = conversion.tof.wavelength_from_tof(tof=wavelength_bounded_spectra.coords["tof"], Ltotal=beam_total) + wavelength_bounded_spectra.coords["tof"] = wavelength_coord + summed_counts += wavelength_bounded_spectra.rebin({"tof": bounds}).sum() + return summed_counts + return sum_spectra_with_wavelength + + class ScalarNormalizer(Reducer, StandardReadable, metaclass=ABCMeta): """Sum a set of user-specified spectra, then normalize by a scalar signal.""" From a3738272992330adade0c9c5dc896ac994425aed Mon Sep 17 00:00:00 2001 From: esmithExperimentControls Date: Thu, 21 Nov 2024 13:47:03 +0000 Subject: [PATCH 07/25] ruff formatting --- .../devices/simpledae/reducers.py | 56 +++++++++++++------ 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/src/ibex_bluesky_core/devices/simpledae/reducers.py b/src/ibex_bluesky_core/devices/simpledae/reducers.py index fa91a00e..f5b0edd0 100644 --- a/src/ibex_bluesky_core/devices/simpledae/reducers.py +++ b/src/ibex_bluesky_core/devices/simpledae/reducers.py @@ -38,11 +38,14 @@ async def sum_spectra(spectra: Collection[DaeSpectra]) -> sc.Variable | sc.DataA return summed_counts -def tof_bounded_spectra(bounds: sc.Variable) -> Callable[[Collection[DaeSpectra]], Awaitable[sc.Variable | sc.DataArray]]: +def tof_bounded_spectra( + bounds: sc.Variable, +) -> Callable[[Collection[DaeSpectra]], Awaitable[sc.Variable | sc.DataArray]]: if "tof" not in bounds.dims: - raise ValueError("Should contain tof dims") # todo write tests covering this + raise ValueError("Should contain tof dims") # todo write tests covering this if bounds.sizes["tof"] != 2: - raise ValueError("Should contain lower and upper bound") # todo write tests covering this + raise ValueError("Should contain lower and upper bound") # todo write tests covering this + async def sum_spectra_with_tof(spectra: Collection[DaeSpectra]) -> sc.Variable | sc.DataArray: """ sums spectra, bounded by a time of flight upper and lower bound @@ -50,35 +53,48 @@ async def sum_spectra_with_tof(spectra: Collection[DaeSpectra]) -> sc.Variable | summed_counts = sc.scalar(value=0, unit=sc.units.counts, dtype="float64") for spec in asyncio.as_completed([s.read_spectrum_dataarray() for s in spectra]): tof_bound_spectra = await spec - summed_counts += tof_bound_spectra.rebin({ - "tof": bounds - }).sum() + summed_counts += tof_bound_spectra.rebin({"tof": bounds}).sum() return summed_counts + return sum_spectra_with_tof -def wavelength_bounded_spectra(bounds: sc.Variable, beam_total: sc.Variable) -> Callable[[Collection[DaeSpectra]], Awaitable[sc.Variable | sc.DataArray]]: +def wavelength_bounded_spectra( + bounds: sc.Variable, beam_total: sc.Variable +) -> Callable[[Collection[DaeSpectra]], Awaitable[sc.Variable | sc.DataArray]]: if "tof" not in bounds.dims: raise ValueError("Should contain tof dims") - - async def sum_spectra_with_wavelength(spectra: Collection[DaeSpectra]) -> sc.Variable | sc.DataArray: + + async def sum_spectra_with_wavelength( + spectra: Collection[DaeSpectra], + ) -> sc.Variable | sc.DataArray: """ sums spectra, converting time of flight to wavelength """ summed_counts = sc.scalar(value=0, unit=sc.units.counts, dtype="float64") for spec in asyncio.as_completed([s.read_spectrum_dataarray() for s in spectra]): wavelength_bounded_spectra = await spec - wavelength_coord = conversion.tof.wavelength_from_tof(tof=wavelength_bounded_spectra.coords["tof"], Ltotal=beam_total) + wavelength_coord = conversion.tof.wavelength_from_tof( + tof=wavelength_bounded_spectra.coords["tof"], Ltotal=beam_total + ) wavelength_bounded_spectra.coords["tof"] = wavelength_coord summed_counts += wavelength_bounded_spectra.rebin({"tof": bounds}).sum() return summed_counts + return sum_spectra_with_wavelength class ScalarNormalizer(Reducer, StandardReadable, metaclass=ABCMeta): """Sum a set of user-specified spectra, then normalize by a scalar signal.""" - def __init__(self, prefix: str, detector_spectra: Sequence[int], summer: Callable[[Collection[DaeSpectra]], Awaitable[sc.Variable | sc.DataArray]] = sum_spectra) -> None: + def __init__( + self, + prefix: str, + detector_spectra: Sequence[int], + summer: Callable[ + [Collection[DaeSpectra]], Awaitable[sc.Variable | sc.DataArray] + ] = sum_spectra, + ) -> None: """Init. Args: @@ -164,11 +180,16 @@ class MonitorNormalizer(Reducer, StandardReadable): """Normalize a set of user-specified detector spectra by user-specified monitor spectra.""" def __init__( - self, prefix: str, detector_spectra: Sequence[int], monitor_spectra: Sequence[int], - detector_summer: Callable[[Collection[DaeSpectra]], - Awaitable[sc.Variable | sc.DataArray]] = sum_spectra, - monitor_summer: Callable[[Collection[DaeSpectra]], - Awaitable[sc.Variable | sc.DataArray]] = sum_spectra + self, + prefix: str, + detector_spectra: Sequence[int], + monitor_spectra: Sequence[int], + detector_summer: Callable[ + [Collection[DaeSpectra]], Awaitable[sc.Variable | sc.DataArray] + ] = sum_spectra, + monitor_summer: Callable[ + [Collection[DaeSpectra]], Awaitable[sc.Variable | sc.DataArray] + ] = sum_spectra, ) -> None: """Init. @@ -209,7 +230,8 @@ def __init__( async def reduce_data(self, dae: "SimpleDae") -> None: """Apply the normalization.""" detector_counts, monitor_counts = await asyncio.gather( - self.detector_summer(self.detectors.values()), self.monitor_summer(self.monitors.values()) + self.detector_summer(self.detectors.values()), + self.monitor_summer(self.monitors.values()), ) if monitor_counts.value == 0.0: # To avoid zero division From 873452e3ff70e82af2442befbf52e0d09eea4058 Mon Sep 17 00:00:00 2001 From: esmithExperimentControls Date: Thu, 21 Nov 2024 13:48:52 +0000 Subject: [PATCH 08/25] ruff formatting, add fixtures and tests for monitor normalizer for tof bound and wavelength bounding of spectra --- tests/devices/simpledae/test_reducers.py | 598 +++++++++++++++++++++-- 1 file changed, 544 insertions(+), 54 deletions(-) diff --git a/tests/devices/simpledae/test_reducers.py b/tests/devices/simpledae/test_reducers.py index c4f89d5e..7549f662 100644 --- a/tests/devices/simpledae/test_reducers.py +++ b/tests/devices/simpledae/test_reducers.py @@ -3,6 +3,7 @@ import pytest import scipp as sc +import scippneutron as scn from ophyd_async.core import set_mock_value from ibex_bluesky_core.devices.simpledae import SimpleDae @@ -10,9 +11,12 @@ GoodFramesNormalizer, MonitorNormalizer, PeriodGoodFramesNormalizer, - ScalarNormalizer + ScalarNormalizer, +) +from ibex_bluesky_core.devices.simpledae.reducers import ( + tof_bounded_spectra, + wavelength_bounded_spectra, ) -from ibex_bluesky_core.devices.simpledae.reducers import tof_bounded_spectra @pytest.fixture @@ -29,6 +33,7 @@ async def good_frames_reducer() -> GoodFramesNormalizer: return reducer +# detector summer sum_spectra/default, monitor summer sum_spectra/default 1, 1 @pytest.fixture async def monitor_normalizer() -> MonitorNormalizer: reducer = MonitorNormalizer(prefix="", detector_spectra=[1], monitor_spectra=[2]) @@ -36,10 +41,181 @@ async def monitor_normalizer() -> MonitorNormalizer: return reducer +# detector summer sum_spectra/default, monitor summer tof_bounded 1, 2 +@pytest.fixture +async def monitor_normalizer_tof_bounded_zero_to_one_half_det_normal_mon_tof() -> MonitorNormalizer: + reducer = MonitorNormalizer( + prefix="", + detector_spectra=[1], + monitor_spectra=[2], + monitor_summer=tof_bounded_spectra( + sc.array(dims=["tof"], values=[0.0, 0.5], unit=sc.units.us) + ), + ) + await reducer.connect(mock=True) + return reducer + + +# detector summer sum_spectra/default, monitor summer wavelength 1, 3 +@pytest.fixture +async def monitor_normalizer_det_normal_mon_wavelenth() -> MonitorNormalizer: + reducer = MonitorNormalizer( + prefix="", + detector_spectra=[1], + monitor_spectra=[2], + monitor_summer=wavelength_bounded_spectra( + bounds=sc.array( + dims=["tof"], values=[0.0, 5.1], unit=sc.units.angstrom, dtype="float64" + ), + beam_total=sc.scalar(value=10.0, unit=sc.units.m), + ), + ) + await reducer.connect(mock=True) + return reducer + + +# detector summer tof_bounded, monitor summer sum_spectra/default 2, 1 +@pytest.fixture +async def monitor_normalizer_tof_bounded_zero_to_one_half_det_tof_mon_normal() -> MonitorNormalizer: + reducer = MonitorNormalizer( + prefix="", + detector_spectra=[1], + monitor_spectra=[2], + detector_summer=tof_bounded_spectra( + sc.array(dims=["tof"], values=[0.0, 0.5], unit=sc.units.us) + ), + ) + await reducer.connect(mock=True) + return reducer + + +# detector summer tof_bounded, monitor summer tof_bounded 2, 2 +@pytest.fixture +async def monitor_normalizer_tof_bounded_one_to_three() -> MonitorNormalizer: + reducer = MonitorNormalizer( + prefix="", + detector_spectra=[1], + monitor_spectra=[2], + detector_summer=tof_bounded_spectra( + sc.array(dims=["tof"], values=[0.0, 3.0], unit=sc.units.us) + ), + monitor_summer=tof_bounded_spectra( + sc.array(dims=["tof"], values=[0.0, 3.0], unit=sc.units.us) + ), + ) + await reducer.connect(mock=True) + return reducer + + +@pytest.fixture +async def monitor_normalizer_tof_bounded_zero_to_one_half() -> MonitorNormalizer: + reducer = MonitorNormalizer( + prefix="", + detector_spectra=[1], + monitor_spectra=[2], + detector_summer=tof_bounded_spectra( + sc.array(dims=["tof"], values=[0.0, 0.5], unit=sc.units.us) + ), + monitor_summer=tof_bounded_spectra( + sc.array(dims=["tof"], values=[0.0, 0.5], unit=sc.units.us) + ), + ) + await reducer.connect(mock=True) + return reducer + + +# Monitor Normalizer detector summer tof_bounded, monitor summer wavelength 2, 3 +@pytest.fixture +async def monitor_normalizer_det_tof_mon_wavelenth() -> MonitorNormalizer: + reducer = MonitorNormalizer( + prefix="", + detector_spectra=[1], + monitor_spectra=[2], + detector_summer=tof_bounded_spectra( + sc.array(dims=["tof"], values=[0.0, 0.5], unit=sc.units.us) + ), + monitor_summer=wavelength_bounded_spectra( + bounds=sc.array( + dims=["tof"], values=[0.0, 5.1], unit=sc.units.angstrom, dtype="float64" + ), + beam_total=sc.scalar(value=10.0, unit=sc.units.m), + ), + ) + await reducer.connect(mock=True) + return reducer + + +# Monitor Normalizer detector summer wavelength bounded, monitor summer sum_spectra/default 3, 1 +@pytest.fixture +async def monitor_normalizer_det_wavelength_mon_normal() -> MonitorNormalizer: + reducer = MonitorNormalizer( + prefix="", + detector_spectra=[1], + monitor_spectra=[2], + detector_summer=wavelength_bounded_spectra( + bounds=sc.array( + dims=["tof"], values=[0.0, 5.1], unit=sc.units.angstrom, dtype="float64" + ), + beam_total=sc.scalar(value=10.0, unit=sc.units.m), + ), + ) + await reducer.connect(mock=True) + return reducer + + +# Monitor Normalizer detector summer wavelength bounded, monitor summer tof_bounded 3, 2 +@pytest.fixture +async def monitor_normalizer_det_wavelength_mon_tof() -> MonitorNormalizer: + reducer = MonitorNormalizer( + prefix="", + detector_spectra=[1], + monitor_spectra=[2], + detector_summer=wavelength_bounded_spectra( + bounds=sc.array( + dims=["tof"], values=[0.0, 5.1], unit=sc.units.angstrom, dtype="float64" + ), + beam_total=sc.scalar(value=10.0, unit=sc.units.m), + ), + monitor_summer=tof_bounded_spectra( + sc.array(dims=["tof"], values=[0.0, 0.5], unit=sc.units.us) + ), + ) + await reducer.connect(mock=True) + return reducer + + +# Monitor Normalizer detector summer wavelength bounded, monitor summer wavelength 3, 3 +@pytest.fixture +async def monitor_normalizer_det_wavelength_mon_wavelength() -> MonitorNormalizer: + reducer = MonitorNormalizer( + prefix="", + detector_spectra=[1], + monitor_spectra=[2], + detector_summer=wavelength_bounded_spectra( + bounds=sc.array( + dims=["tof"], values=[0.0, 5.1], unit=sc.units.angstrom, dtype="float64" + ), + beam_total=sc.scalar(value=10.0, unit=sc.units.m), + ), + monitor_summer=wavelength_bounded_spectra( + bounds=sc.array( + dims=["tof"], values=[0.0, 4.5], unit=sc.units.angstrom, dtype="float64" + ), + beam_total=sc.scalar(value=10.0, unit=sc.units.m), + ), + ) + await reducer.connect(mock=True) + return reducer + + @pytest.fixture async def scalar_normalizer_bounded_sum_zero_to_one_half(simpledae: SimpleDae) -> ScalarNormalizer: set_mock_value(simpledae.period.good_frames, 1) - reducer = PeriodGoodFramesNormalizer(prefix="", detector_spectra=[1,2], summer=tof_bounded_spectra(sc.array(dims=["tof"], values=[0, 0.5], unit=sc.units.us))) + reducer = PeriodGoodFramesNormalizer( + prefix="", + detector_spectra=[1, 2], + summer=tof_bounded_spectra(sc.array(dims=["tof"], values=[0, 0.5], unit=sc.units.us)), + ) await reducer.connect(mock=True) return reducer @@ -47,25 +223,84 @@ async def scalar_normalizer_bounded_sum_zero_to_one_half(simpledae: SimpleDae) - @pytest.fixture async def scalar_normalizer_bounded_sum_one_to_one(simpledae: SimpleDae) -> ScalarNormalizer: set_mock_value(simpledae.period.good_frames, 1) - reducer = PeriodGoodFramesNormalizer(prefix="", detector_spectra=[1,2], summer=tof_bounded_spectra(sc.array(dims=["tof"], values=[1.0, 1.0], unit=sc.units.us))) + reducer = PeriodGoodFramesNormalizer( + prefix="", + detector_spectra=[1, 2], + summer=tof_bounded_spectra(sc.array(dims=["tof"], values=[1.0, 1.0], unit=sc.units.us)), + ) await reducer.connect(mock=True) - return reducer - + return reducer + + +@pytest.fixture +async def scalar_normalizer_bounded_sum_one_to_three_micro_sec( + simpledae: SimpleDae, +) -> ScalarNormalizer: + set_mock_value(simpledae.period.good_frames, 1) + reducer = PeriodGoodFramesNormalizer( + prefix="", + detector_spectra=[1, 2], + summer=tof_bounded_spectra(sc.array(dims=["tof"], values=[0.0, 3.0], unit=sc.units.us)), + ) + await reducer.connect(mock=True) + return reducer + @pytest.fixture async def spectra_bins_easy_to_test() -> sc.DataArray: return sc.DataArray( - data=sc.Variable(dims=["tof"], values=[1000.0, 2000.0, 3000.0, 2000.0, 1000.0], unit=sc.units.counts, dtype="float64"), - coords={"tof": sc.array(dims=["tof"], values=[0, 1, 2, 3, 4, 5], unit=sc.units.us, dtype="float64")}, + data=sc.Variable( + dims=["tof"], + values=[1000.0, 2000.0, 3000.0, 2000.0, 1000.0], + unit=sc.units.counts, + dtype="float64", + ), + coords={ + "tof": sc.array( + dims=["tof"], values=[0, 1, 2, 3, 4, 5], unit=sc.units.us, dtype="float64" + ) + }, ) @pytest.fixture -async def scalar_normalizer_unbounded_sum(simpledae: SimpleDae) -> ScalarNormalizer: - set_mock_value(simpledae.period.good_frames, 1) - reducer = PeriodGoodFramesNormalizer(prefix="", detector_spectra=[1,2], summer=tof_bounded_spectra(sc.array(dims=["tof"], values=[], unit=sc.units.us))) - await reducer.connect(mock=True) - return reducer +async def spectra_bins_tof_convert_to_wavelength_easy() -> sc.DataArray: + return sc.DataArray( + data=sc.Variable( + dims=["tof"], + values=[1000.0, 2000.0, 3000.0, 2000.0, 1000.0], + unit=sc.units.counts, + dtype="float64", + ), + coords={ + "tof": sc.array( + dims=["tof"], + values=[10000, 11000, 12000, 13000, 14000, 15000], + unit=sc.units.us, + dtype="float64", + ) + }, + ) + + +@pytest.fixture +async def spectra_bins_tof_convert_to_wavelength_easy_copy() -> sc.DataArray: + return sc.DataArray( + data=sc.Variable( + dims=["tof"], + values=[1000.0, 2000.0, 3000.0, 2000.0, 1000.0], + unit=sc.units.counts, + dtype="float64", + ), + coords={ + "tof": sc.array( + dims=["tof"], + values=[10000, 11000, 12000, 13000, 14000, 15000], + unit=sc.units.us, + dtype="float64", + ) + }, + ) class FakePeriod: @@ -168,7 +403,11 @@ async def test_period_good_frames_normalizer_uncertainties( variances=[4000.0, 5000.0, 6000.0], unit=sc.units.counts, ), - coords={"tof": sc.array(dims=["tof"], values=[0, 1, 2, 3], unit=sc.units.us, dtype="float64")}, + coords={ + "tof": sc.array( + dims=["tof"], values=[0, 1, 2, 3], unit=sc.units.us, dtype="float64" + ) + }, ) ) @@ -192,7 +431,11 @@ async def test_period_good_frames_normalizer_zero_counts( variances=[0.0, 0.0, 0.0], unit=sc.units.counts, ), - coords={"tof": sc.array(dims=["tof"], values=[0, 1, 2, 3], unit=sc.units.us, dtype="float64")}, + coords={ + "tof": sc.array( + dims=["tof"], values=[0, 1, 2, 3], unit=sc.units.us, dtype="float64" + ) + }, ) ) period_good_frames_reducer.detectors[2].read_spectrum_dataarray = AsyncMock( @@ -203,7 +446,11 @@ async def test_period_good_frames_normalizer_zero_counts( variances=[0.0, 0.0, 0.0], unit=sc.units.counts, ), - coords={"tof": sc.array(dims=["tof"], values=[0, 1, 2, 3], unit=sc.units.us, dtype="float64")}, + coords={ + "tof": sc.array( + dims=["tof"], values=[0, 1, 2, 3], unit=sc.units.us, dtype="float64" + ) + }, ) ) @@ -216,10 +463,79 @@ async def test_period_good_frames_normalizer_zero_counts( assert intensity_stddev == 0 +async def test_scalar_normalizer_tof_bounded_zero_to_one_half( + simpledae: SimpleDae, scalar_normalizer_bounded_sum_zero_to_one_half: ScalarNormalizer +): + scalar_normalizer_bounded_sum_zero_to_one_half.detectors[1].read_spectrum_dataarray = AsyncMock( + return_value=sc.DataArray( + data=sc.Variable( + dims=["tof"], + values=[1000.0, 2000.0, 3000.0], + variances=[1000.0, 2000.0, 3000.0], + unit=sc.units.counts, + dtype="float64", + ), + coords={ + "tof": sc.array( + dims=["tof"], values=[0, 1, 2, 3], unit=sc.units.us, dtype="float64" + ) + }, + ) + ) + scalar_normalizer_bounded_sum_zero_to_one_half.detectors[2].read_spectrum_dataarray = AsyncMock( + return_value=sc.DataArray( + data=sc.Variable( + dims=["tof"], + values=[4000.0, 5000.0, 6000.0], + variances=[4000.0, 5000.0, 6000.0], + unit=sc.units.counts, + dtype="float64", + ), + coords={ + "tof": sc.array( + dims=["tof"], values=[0, 1, 2, 3], unit=sc.units.us, dtype="float64" + ) + }, + ) + ) + + await scalar_normalizer_bounded_sum_zero_to_one_half.reduce_data(simpledae) + + det_counts = await scalar_normalizer_bounded_sum_zero_to_one_half.det_counts.get_value() + intensity = await scalar_normalizer_bounded_sum_zero_to_one_half.intensity.get_value() + + assert det_counts == 2500 # 500 from first detector + 2000 from second detector + assert intensity == pytest.approx(2500) + + +async def test_scalar_normalizer_tof_bounded_upper_and_lower_bound_equal( + simpledae: SimpleDae, + scalar_normalizer_bounded_sum_one_to_one: ScalarNormalizer, + spectra_bins_easy_to_test: sc.DataArray, +): + scalar_normalizer_bounded_sum_one_to_one.detectors[1].read_spectrum_dataarray = AsyncMock( + return_value=spectra_bins_easy_to_test + ) + + scalar_normalizer_bounded_sum_one_to_one.detectors[2].read_spectrum_dataarray = AsyncMock( + return_value=spectra_bins_easy_to_test + ) + + await scalar_normalizer_bounded_sum_one_to_one.reduce_data(simpledae) + + det_counts = await scalar_normalizer_bounded_sum_one_to_one.det_counts.get_value() + intensity = await scalar_normalizer_bounded_sum_one_to_one.intensity.get_value() + + assert det_counts == 0 # 0 from first detector + 0 from second detector + assert intensity == pytest.approx(0) + + # Monitor Normalizer -async def test_monitor_normalizer(simpledae: SimpleDae, monitor_normalizer: MonitorNormalizer): +async def test_monitor_normalizer( + simpledae: SimpleDae, monitor_normalizer: MonitorNormalizer +): # 1, 1 monitor_normalizer.detectors[1].read_spectrum_dataarray = AsyncMock( return_value=sc.DataArray( data=sc.Variable(dims=["tof"], values=[1000.0, 2000.0, 3000.0], unit=sc.units.counts), @@ -337,25 +653,127 @@ async def test_monitor_normalizer_publishes_raw_and_normalized_count_uncertainti assert monitor_normalizer.det_counts_stddev in readables assert monitor_normalizer.mon_counts_stddev in readables -async def test_tof_bounded_spectra_half_of_first_bin_rebinned_correctly( + +async def test_monitor_normalizer_det_sum_normal_mon_sum_tof_bound_( # 1, 2 simpledae: SimpleDae, - scalar_normalizer_bounded_sum_zero_to_one_half: ScalarNormalizer - ): + monitor_normalizer_tof_bounded_zero_to_one_half_det_normal_mon_tof: MonitorNormalizer, + spectra_bins_easy_to_test: sc.DataArray, +): + monitor_normalizer_tof_bounded_zero_to_one_half_det_normal_mon_tof.detectors[ + 1 + ].read_spectrum_dataarray = AsyncMock(return_value=spectra_bins_easy_to_test) - scalar_normalizer_bounded_sum_zero_to_one_half.detectors[1].read_spectrum_dataarray = AsyncMock( + monitor_normalizer_tof_bounded_zero_to_one_half_det_normal_mon_tof.monitors[ + 2 + ].read_spectrum_dataarray = AsyncMock(return_value=spectra_bins_easy_to_test) + + await monitor_normalizer_tof_bounded_zero_to_one_half_det_normal_mon_tof.reduce_data(simpledae) + + det_counts = await monitor_normalizer_tof_bounded_zero_to_one_half_det_normal_mon_tof.det_counts.get_value() + mon_counts = await monitor_normalizer_tof_bounded_zero_to_one_half_det_normal_mon_tof.mon_counts.get_value() + intensity = await monitor_normalizer_tof_bounded_zero_to_one_half_det_normal_mon_tof.intensity.get_value() + + assert det_counts == 9000.0 # 1 + 2 + 3 + 2 + 1 from detector = 9 + assert mon_counts == 500.0 # 1k / 2k = 500 from monitor + assert intensity == pytest.approx(9000.0 / 500.0) + + +# test_monitor_normalizer_det_sum_normal_mon_sum_wavelength 1, 3 +async def test_monitor_normalizer_det_sum_normal_mon_sum_wavelenth( # 1, 3 + simpledae: SimpleDae, + monitor_normalizer_det_normal_mon_wavelenth: MonitorNormalizer, + spectra_bins_easy_to_test: sc.DataArray, + spectra_bins_tof_convert_to_wavelength_easy: sc.DataArray, +): + monitor_normalizer_det_normal_mon_wavelenth.detectors[1].read_spectrum_dataarray = AsyncMock( + return_value=spectra_bins_easy_to_test + ) + + monitor_normalizer_det_normal_mon_wavelenth.monitors[2].read_spectrum_dataarray = AsyncMock( + return_value=spectra_bins_tof_convert_to_wavelength_easy + ) + + await monitor_normalizer_det_normal_mon_wavelenth.reduce_data(simpledae) + + det_counts = await monitor_normalizer_det_normal_mon_wavelenth.det_counts.get_value() + mon_counts = await monitor_normalizer_det_normal_mon_wavelenth.mon_counts.get_value() + intensity = await monitor_normalizer_det_normal_mon_wavelenth.intensity.get_value() + + assert det_counts == 9000.0 # 1 + 2 + 3 + 2 + 1 from detector = 9 + assert mon_counts == pytest.approx(5675.097) # angstrom rebinning from monitor + assert intensity == pytest.approx(9000.0 / 5675.097) + + +async def test_monitor_normalizer_det_sum_tof_bound_mon_sum_normal( # 2, 1 + simpledae: SimpleDae, + monitor_normalizer_tof_bounded_zero_to_one_half_det_tof_mon_normal: MonitorNormalizer, + spectra_bins_easy_to_test: sc.DataArray, +): + monitor_normalizer_tof_bounded_zero_to_one_half_det_tof_mon_normal.detectors[ + 1 + ].read_spectrum_dataarray = AsyncMock(return_value=spectra_bins_easy_to_test) + + monitor_normalizer_tof_bounded_zero_to_one_half_det_tof_mon_normal.monitors[ + 2 + ].read_spectrum_dataarray = AsyncMock(return_value=spectra_bins_easy_to_test) + + await monitor_normalizer_tof_bounded_zero_to_one_half_det_tof_mon_normal.reduce_data(simpledae) + + det_counts = await monitor_normalizer_tof_bounded_zero_to_one_half_det_tof_mon_normal.det_counts.get_value() + mon_counts = await monitor_normalizer_tof_bounded_zero_to_one_half_det_tof_mon_normal.mon_counts.get_value() + intensity = await monitor_normalizer_tof_bounded_zero_to_one_half_det_tof_mon_normal.intensity.get_value() + + assert det_counts == 500 # 1 + 2 + 3 from detector = 6 + assert mon_counts == 9000.0 # 1 + 2 + 3 from monitor = 6 + assert intensity == pytest.approx(500 / 9000.0) + + +async def test_monitor_normalizer_det_and_mon_summer_tof_bounded( # 2, 2 + simpledae: SimpleDae, + monitor_normalizer_tof_bounded_one_to_three: MonitorNormalizer, + spectra_bins_easy_to_test: sc.DataArray, +): + monitor_normalizer_tof_bounded_one_to_three.detectors[1].read_spectrum_dataarray = AsyncMock( + return_value=spectra_bins_easy_to_test + ) + + monitor_normalizer_tof_bounded_one_to_three.monitors[2].read_spectrum_dataarray = AsyncMock( + return_value=spectra_bins_easy_to_test + ) + + await monitor_normalizer_tof_bounded_one_to_three.reduce_data(simpledae) + + det_counts = await monitor_normalizer_tof_bounded_one_to_three.det_counts.get_value() + mon_counts = await monitor_normalizer_tof_bounded_one_to_three.mon_counts.get_value() + intensity = await monitor_normalizer_tof_bounded_one_to_three.intensity.get_value() + + assert det_counts == 6000.0 # 1 + 2 + 3 from detector = 6 + assert mon_counts == 6000.0 # 1 + 2 + 3 from monitor = 6 + assert intensity == pytest.approx(1.0) + + +async def test_monitor_normalizer_det_mon_summer_tof_bounded_zero_to_one_half( # 2, 2 + simpledae: SimpleDae, monitor_normalizer_tof_bounded_zero_to_one_half: MonitorNormalizer +): + monitor_normalizer_tof_bounded_zero_to_one_half.detectors[ + 1 + ].read_spectrum_dataarray = AsyncMock( return_value=sc.DataArray( data=sc.Variable( dims=["tof"], values=[1000.0, 2000.0, 3000.0], variances=[1000.0, 2000.0, 3000.0], unit=sc.units.counts, - dtype="float64" + dtype="float64", ), - coords={"tof": sc.array(dims=["tof"], values=[0, 1, 2, 3], unit=sc.units.us, - dtype="float64")}, + coords={ + "tof": sc.array( + dims=["tof"], values=[0, 1, 2, 3], unit=sc.units.us, dtype="float64" + ) + }, ) ) - scalar_normalizer_bounded_sum_zero_to_one_half.detectors[2].read_spectrum_dataarray = AsyncMock( + monitor_normalizer_tof_bounded_zero_to_one_half.monitors[2].read_spectrum_dataarray = AsyncMock( return_value=sc.DataArray( data=sc.Variable( dims=["tof"], @@ -364,60 +782,132 @@ async def test_tof_bounded_spectra_half_of_first_bin_rebinned_correctly( unit=sc.units.counts, dtype="float64", ), - coords={"tof": sc.array(dims=["tof"], values=[0, 1, 2, 3], unit=sc.units.us, - dtype="float64")}, + coords={ + "tof": sc.array( + dims=["tof"], values=[0, 1, 2, 3], unit=sc.units.us, dtype="float64" + ) + }, ) ) - await scalar_normalizer_bounded_sum_zero_to_one_half.reduce_data(simpledae) + await monitor_normalizer_tof_bounded_zero_to_one_half.reduce_data(simpledae) - det_counts = await scalar_normalizer_bounded_sum_zero_to_one_half.det_counts.get_value() - intensity = await scalar_normalizer_bounded_sum_zero_to_one_half.intensity.get_value() + det_counts = await monitor_normalizer_tof_bounded_zero_to_one_half.det_counts.get_value() + mon_counts = await monitor_normalizer_tof_bounded_zero_to_one_half.mon_counts.get_value() + intensity = await monitor_normalizer_tof_bounded_zero_to_one_half.intensity.get_value() - assert det_counts == 2500 # 500 from first detector + 2000 from second detector - assert intensity == pytest.approx(2500) + assert det_counts == 500 # 500 from first detector + assert mon_counts == 2000 # 2000 half of monitor first bin + assert intensity == pytest.approx(0.25) # 500 / 2500 -async def test_tof_bounded_spectra_upper_and_lower_bound_equal( +# test_monitor_normalizer_det_sum_tof_bound_mon_sum_wavelength 2, 3 +async def test_monitor_normalizer_det_sum_tof_mon_sum_wavelength( # 2, 3 simpledae: SimpleDae, - scalar_normalizer_bounded_sum_one_to_one: ScalarNormalizer, - spectra_bins_easy_to_test: sc.DataArray + monitor_normalizer_det_tof_mon_wavelenth: MonitorNormalizer, + spectra_bins_easy_to_test: sc.DataArray, + spectra_bins_tof_convert_to_wavelength_easy: sc.DataArray, ): - - scalar_normalizer_bounded_sum_one_to_one.detectors[1].read_spectrum_dataarray = AsyncMock( + monitor_normalizer_det_tof_mon_wavelenth.detectors[1].read_spectrum_dataarray = AsyncMock( return_value=spectra_bins_easy_to_test ) - scalar_normalizer_bounded_sum_one_to_one.detectors[2].read_spectrum_dataarray = AsyncMock( - return_value=spectra_bins_easy_to_test + monitor_normalizer_det_tof_mon_wavelenth.monitors[2].read_spectrum_dataarray = AsyncMock( + return_value=spectra_bins_tof_convert_to_wavelength_easy ) - await scalar_normalizer_bounded_sum_one_to_one.reduce_data(simpledae) + await monitor_normalizer_det_tof_mon_wavelenth.reduce_data(simpledae) - det_counts = await scalar_normalizer_bounded_sum_one_to_one.det_counts.get_value() - intensity = await scalar_normalizer_bounded_sum_one_to_one.intensity.get_value() + det_counts = await monitor_normalizer_det_tof_mon_wavelenth.det_counts.get_value() + mon_counts = await monitor_normalizer_det_tof_mon_wavelenth.mon_counts.get_value() + intensity = await monitor_normalizer_det_tof_mon_wavelenth.intensity.get_value() - assert det_counts == 0 # 500 from first detector + 2000 from second detector - assert intensity == pytest.approx(0) + assert det_counts == 500.0 # 1000 + (0.5 x 2000) from detector = 2000 + assert mon_counts == pytest.approx(5675.097) # angstrom rebinning from monitor + assert intensity == pytest.approx(500 / 5675.097) -async def test_tof_bounded_spectra_no_upper_bound( +# test_monitor_normalizer_det_sum_wavelength_mon_sum_normal 3, 1 +async def test_monitor_normalizer_det_sum_wavelength_mon_sum_normal( # 3, 1 simpledae: SimpleDae, - scalar_normalizer_unbounded_sum: ScalarNormalizer, - spectra_bins_easy_to_test: sc.DataArray + monitor_normalizer_det_wavelength_mon_normal: MonitorNormalizer, + spectra_bins_easy_to_test: sc.DataArray, + spectra_bins_tof_convert_to_wavelength_easy: sc.DataArray, ): - scalar_normalizer_unbounded_sum.detectors[1].read_spectrum_dataarray = AsyncMock( + monitor_normalizer_det_wavelength_mon_normal.detectors[1].read_spectrum_dataarray = AsyncMock( + return_value=spectra_bins_tof_convert_to_wavelength_easy + ) + + monitor_normalizer_det_wavelength_mon_normal.monitors[2].read_spectrum_dataarray = AsyncMock( return_value=spectra_bins_easy_to_test ) - scalar_normalizer_unbounded_sum.detectors[2].read_spectrum_dataarray = AsyncMock( + await monitor_normalizer_det_wavelength_mon_normal.reduce_data(simpledae) + + det_counts = await monitor_normalizer_det_wavelength_mon_normal.det_counts.get_value() + mon_counts = await monitor_normalizer_det_wavelength_mon_normal.mon_counts.get_value() + intensity = await monitor_normalizer_det_wavelength_mon_normal.intensity.get_value() + + assert det_counts == pytest.approx(5675.097) # 1 + 2 + 3 + 2 + 1 from detector = 9 + assert mon_counts == 9000.0 # angstrom rebinning from monitor + assert intensity == pytest.approx(5675.097 / 9000.0) + + +# test_monitor_normalizer_det_sum_wavelength_mon_sum_tof_bound 3, 2 +async def test_monitor_normalizer_det_sum_wavelength_mon_sum_tof( # 3, 2 + simpledae: SimpleDae, + monitor_normalizer_det_wavelength_mon_tof: MonitorNormalizer, + spectra_bins_easy_to_test: sc.DataArray, + spectra_bins_tof_convert_to_wavelength_easy: sc.DataArray, +): + monitor_normalizer_det_wavelength_mon_tof.detectors[1].read_spectrum_dataarray = AsyncMock( + return_value=spectra_bins_tof_convert_to_wavelength_easy + ) + + monitor_normalizer_det_wavelength_mon_tof.monitors[2].read_spectrum_dataarray = AsyncMock( return_value=spectra_bins_easy_to_test ) - await scalar_normalizer_unbounded_sum.reduce_data(simpledae) + await monitor_normalizer_det_wavelength_mon_tof.reduce_data(simpledae) + + det_counts = await monitor_normalizer_det_wavelength_mon_tof.det_counts.get_value() + mon_counts = await monitor_normalizer_det_wavelength_mon_tof.mon_counts.get_value() + intensity = await monitor_normalizer_det_wavelength_mon_tof.intensity.get_value() + + assert det_counts == pytest.approx(5675.097) # angstrom from detector = 9 + assert mon_counts == 500.0 # tof rebinning from monitor + assert intensity == pytest.approx(5675.097 / 500.0) + + +# test_monitor_normalizer_det_sum_wavelength_mon_sum_wavelength 3, 3 +async def test_monitor_normalizer_det_sum_wavelength_mon_sum_wavelength( # 3, 3 + simpledae: SimpleDae, + monitor_normalizer_det_wavelength_mon_wavelength: MonitorNormalizer, + spectra_bins_tof_convert_to_wavelength_easy: sc.DataArray, + spectra_bins_tof_convert_to_wavelength_easy_copy: sc.DataArray, +): + monitor_normalizer_det_wavelength_mon_wavelength.detectors[ + 1 + ].read_spectrum_dataarray = AsyncMock(return_value=spectra_bins_tof_convert_to_wavelength_easy) + + monitor_normalizer_det_wavelength_mon_wavelength.monitors[ + 2 + ].read_spectrum_dataarray = AsyncMock( + return_value=spectra_bins_tof_convert_to_wavelength_easy_copy + ) + + await monitor_normalizer_det_wavelength_mon_wavelength.reduce_data(simpledae) + + det_counts = await monitor_normalizer_det_wavelength_mon_wavelength.det_counts.get_value() + mon_counts = await monitor_normalizer_det_wavelength_mon_wavelength.mon_counts.get_value() + intensity = await monitor_normalizer_det_wavelength_mon_wavelength.intensity.get_value() + + assert det_counts == pytest.approx(5675.097) # angstrom from detector = 9 + assert mon_counts == pytest.approx(1750.057) # tof rebinning from monitor + assert intensity == pytest.approx(5675.097 / 1750.057) - det_counts = await scalar_normalizer_unbounded_sum.det_counts.get_value() - intensity = await scalar_normalizer_unbounded_sum.intensity.get_value() - assert det_counts == 9000.0 # 1 + 2 + 3 + 2 + 1 thousand = 9 thousand - assert intensity == pytest.approx(9000.0) \ No newline at end of file +@pytest.mark.parametrize("data", [[], [0.0], [0.1, 0.2, 0.3]]) +def test_tof_bounded_spectra_bounds_missing_or_too_many(data: list[float]): + with pytest.raises(expected_exception=ValueError, match="Should contain lower and upper bound"): + tof_bounded_spectra(sc.array(dims=["tof"], values=data, unit=sc.units.us)) From 80bba947b14d92f24abf604910a83dfb1a44ad2d Mon Sep 17 00:00:00 2001 From: esmithExperimentControls Date: Thu, 21 Nov 2024 14:54:28 +0000 Subject: [PATCH 09/25] change dtype of tof variable, as it's explicitly float64 in dae_spectra.py --- tests/devices/test_dae.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/devices/test_dae.py b/tests/devices/test_dae.py index 4a4bc7d7..82a716dd 100644 --- a/tests/devices/test_dae.py +++ b/tests/devices/test_dae.py @@ -985,7 +985,7 @@ async def test_read_spectrum_dataarray(spectrum: DaeSpectra): ), coords={ "tof": sc.Variable( - dims=["tof"], values=[0, 1, 2, 3], dtype="float32", unit=sc.units.us + dims=["tof"], values=[0, 1, 2, 3], dtype="float64", unit=sc.units.us ) }, ), From d4a6cf0f30ac41cd10a6514301573e2897ca2578 Mon Sep 17 00:00:00 2001 From: esmithExperimentControls Date: Fri, 22 Nov 2024 13:28:42 +0000 Subject: [PATCH 10/25] Add tests to cover case of missing tof in dimensions of bounds parameter for tof_bounded_spectra and wavelength_bounded_spectra --- tests/devices/simpledae/test_reducers.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/devices/simpledae/test_reducers.py b/tests/devices/simpledae/test_reducers.py index 7549f662..198e935b 100644 --- a/tests/devices/simpledae/test_reducers.py +++ b/tests/devices/simpledae/test_reducers.py @@ -911,3 +911,15 @@ async def test_monitor_normalizer_det_sum_wavelength_mon_sum_wavelength( # 3, 3 def test_tof_bounded_spectra_bounds_missing_or_too_many(data: list[float]): with pytest.raises(expected_exception=ValueError, match="Should contain lower and upper bound"): tof_bounded_spectra(sc.array(dims=["tof"], values=data, unit=sc.units.us)) + + +def test_tof_bounded_spectra_missing_tof_in_bounds_dims(): + with pytest.raises(expected_exception=ValueError): + tof_bounded_spectra(sc.array(dims=[""], values=[0], unit=sc.units.us)) + + +def test_wavelength_bounded_spectra_missing_tof_in_bounds_dims(): + with pytest.raises(expected_exception=ValueError): + wavelength_bounded_spectra( + bounds=sc.array(dims=[""], values=[0], unit=sc.units.us), beam_total=sc.scalar(value=0) + ) From 9c51a75a164034df30d76263901f714830bc4fd8 Mon Sep 17 00:00:00 2001 From: esmithExperimentControls Date: Mon, 25 Nov 2024 09:16:39 +0000 Subject: [PATCH 11/25] ruff changes --- src/ibex_bluesky_core/devices/dae/dae_spectra.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/ibex_bluesky_core/devices/dae/dae_spectra.py b/src/ibex_bluesky_core/devices/dae/dae_spectra.py index 35a4b83e..93380da7 100644 --- a/src/ibex_bluesky_core/devices/dae/dae_spectra.py +++ b/src/ibex_bluesky_core/devices/dae/dae_spectra.py @@ -128,9 +128,6 @@ async def read_spectrum_dataarray(self) -> sc.DataArray: dtype="float64", ), coords={ - "tof": sc.array(dims=["tof"], - values=tof_edges, - unit=sc.Unit(unit), - dtype="float64") + "tof": sc.array(dims=["tof"], values=tof_edges, unit=sc.Unit(unit), dtype="float64") }, ) From baf675d3cebf2afd62e1ace77a925237c6add27b Mon Sep 17 00:00:00 2001 From: esmithExperimentControls Date: Mon, 25 Nov 2024 10:41:03 +0000 Subject: [PATCH 12/25] Ruff formatting, sphinx documentation changes --- .../devices/simpledae/reducers.py | 30 +++++++++----- tests/devices/simpledae/test_reducers.py | 41 +++++++++---------- 2 files changed, 38 insertions(+), 33 deletions(-) diff --git a/src/ibex_bluesky_core/devices/simpledae/reducers.py b/src/ibex_bluesky_core/devices/simpledae/reducers.py index 18a089b0..2436ea88 100644 --- a/src/ibex_bluesky_core/devices/simpledae/reducers.py +++ b/src/ibex_bluesky_core/devices/simpledae/reducers.py @@ -4,10 +4,9 @@ import logging import math from abc import ABCMeta, abstractmethod -from typing import TYPE_CHECKING, Collection, Sequence +from typing import TYPE_CHECKING, Awaitable, Callable, Collection, Sequence import scipp as sc -from scippneutron import conversion from ophyd_async.core import ( Device, DeviceVector, @@ -15,7 +14,7 @@ StandardReadable, soft_signal_r_and_setter, ) -from typing import Callable, Awaitable +from scippneutron import conversion from ibex_bluesky_core.devices.dae.dae_spectra import DaeSpectra from ibex_bluesky_core.devices.simpledae.strategies import Reducer @@ -46,15 +45,18 @@ async def sum_spectra(spectra: Collection[DaeSpectra]) -> sc.Variable | sc.DataA def tof_bounded_spectra( bounds: sc.Variable, ) -> Callable[[Collection[DaeSpectra]], Awaitable[sc.Variable | sc.DataArray]]: + """Rebin spectra bounded by time of flight bounds. + + Returns a scipp scalar, which has .value and .variance properties for accessing the sum + and variance respectively of the summed counts. + """ if "tof" not in bounds.dims: - raise ValueError("Should contain tof dims") # todo write tests covering this + raise ValueError("Should contain tof dims") if bounds.sizes["tof"] != 2: - raise ValueError("Should contain lower and upper bound") # todo write tests covering this + raise ValueError("Should contain lower and upper bound") async def sum_spectra_with_tof(spectra: Collection[DaeSpectra]) -> sc.Variable | sc.DataArray: - """ - sums spectra, bounded by a time of flight upper and lower bound - """ + """Sum spectra bounded by a time of flight upper and lower bound.""" summed_counts = sc.scalar(value=0, unit=sc.units.counts, dtype="float64") for spec in asyncio.as_completed([s.read_spectrum_dataarray() for s in spectra]): tof_bound_spectra = await spec @@ -67,15 +69,18 @@ async def sum_spectra_with_tof(spectra: Collection[DaeSpectra]) -> sc.Variable | def wavelength_bounded_spectra( bounds: sc.Variable, beam_total: sc.Variable ) -> Callable[[Collection[DaeSpectra]], Awaitable[sc.Variable | sc.DataArray]]: + """Rebin spectra bounded by wavelength bounds. + + Returns a scipp scalar, which has .value and .variance properties for accessing the sum + and variance respectively of the summed counts. + """ if "tof" not in bounds.dims: raise ValueError("Should contain tof dims") async def sum_spectra_with_wavelength( spectra: Collection[DaeSpectra], ) -> sc.Variable | sc.DataArray: - """ - sums spectra, converting time of flight to wavelength - """ + """Sum spectra and convert time of flight to wavelength.""" summed_counts = sc.scalar(value=0, unit=sc.units.counts, dtype="float64") for spec in asyncio.as_completed([s.read_spectrum_dataarray() for s in spectra]): wavelength_bounded_spectra = await spec @@ -105,6 +110,7 @@ def __init__( Args: prefix: the PV prefix of the instrument to get spectra from (e.g. IN:DEMO:) detector_spectra: a sequence of spectra numbers (detectors) to sum. + summer: either sum_spectra, tof_bounded_spectra, or wavelength_bounded_spectra. """ self.detectors = DeviceVector( @@ -204,6 +210,8 @@ def __init__( prefix: the PV prefix of the instrument to get spectra from (e.g. IN:DEMO:) detector_spectra: a sequence of spectra numbers (detectors) to sum. monitor_spectra: a sequence of spectra number (monitors) to sum and normalize by. + detector_summer: either sum_spectra, tof_bounded_spectra, or wavelength_bounded_spectra. + monitor_summer: either sum_spectra, tof_bounded_spectra, or wavelength_bounded_spectra. """ dae_prefix = prefix + "DAE:" diff --git a/tests/devices/simpledae/test_reducers.py b/tests/devices/simpledae/test_reducers.py index 198e935b..6d32a1c6 100644 --- a/tests/devices/simpledae/test_reducers.py +++ b/tests/devices/simpledae/test_reducers.py @@ -3,7 +3,6 @@ import pytest import scipp as sc -import scippneutron as scn from ophyd_async.core import set_mock_value from ibex_bluesky_core.devices.simpledae import SimpleDae @@ -12,8 +11,6 @@ MonitorNormalizer, PeriodGoodFramesNormalizer, ScalarNormalizer, -) -from ibex_bluesky_core.devices.simpledae.reducers import ( tof_bounded_spectra, wavelength_bounded_spectra, ) @@ -43,7 +40,7 @@ async def monitor_normalizer() -> MonitorNormalizer: # detector summer sum_spectra/default, monitor summer tof_bounded 1, 2 @pytest.fixture -async def monitor_normalizer_tof_bounded_zero_to_one_half_det_normal_mon_tof() -> MonitorNormalizer: +async def monitor_normalizer_zero_to_one_half_det_norm_mon_tof() -> MonitorNormalizer: reducer = MonitorNormalizer( prefix="", detector_spectra=[1], @@ -76,7 +73,7 @@ async def monitor_normalizer_det_normal_mon_wavelenth() -> MonitorNormalizer: # detector summer tof_bounded, monitor summer sum_spectra/default 2, 1 @pytest.fixture -async def monitor_normalizer_tof_bounded_zero_to_one_half_det_tof_mon_normal() -> MonitorNormalizer: +async def monitor_normalizer_zero_to_one_half_det_tof_mon_normal() -> MonitorNormalizer: reducer = MonitorNormalizer( prefix="", detector_spectra=[1], @@ -656,22 +653,22 @@ async def test_monitor_normalizer_publishes_raw_and_normalized_count_uncertainti async def test_monitor_normalizer_det_sum_normal_mon_sum_tof_bound_( # 1, 2 simpledae: SimpleDae, - monitor_normalizer_tof_bounded_zero_to_one_half_det_normal_mon_tof: MonitorNormalizer, + monitor_normalizer_zero_to_one_half_det_norm_mon_tof: MonitorNormalizer, spectra_bins_easy_to_test: sc.DataArray, ): - monitor_normalizer_tof_bounded_zero_to_one_half_det_normal_mon_tof.detectors[ + monitor_normalizer_zero_to_one_half_det_norm_mon_tof.detectors[ 1 ].read_spectrum_dataarray = AsyncMock(return_value=spectra_bins_easy_to_test) - monitor_normalizer_tof_bounded_zero_to_one_half_det_normal_mon_tof.monitors[ + monitor_normalizer_zero_to_one_half_det_norm_mon_tof.monitors[ 2 ].read_spectrum_dataarray = AsyncMock(return_value=spectra_bins_easy_to_test) - await monitor_normalizer_tof_bounded_zero_to_one_half_det_normal_mon_tof.reduce_data(simpledae) + await monitor_normalizer_zero_to_one_half_det_norm_mon_tof.reduce_data(simpledae) - det_counts = await monitor_normalizer_tof_bounded_zero_to_one_half_det_normal_mon_tof.det_counts.get_value() - mon_counts = await monitor_normalizer_tof_bounded_zero_to_one_half_det_normal_mon_tof.mon_counts.get_value() - intensity = await monitor_normalizer_tof_bounded_zero_to_one_half_det_normal_mon_tof.intensity.get_value() + det_counts = await monitor_normalizer_zero_to_one_half_det_norm_mon_tof.det_counts.get_value() + mon_counts = await monitor_normalizer_zero_to_one_half_det_norm_mon_tof.mon_counts.get_value() + intensity = await monitor_normalizer_zero_to_one_half_det_norm_mon_tof.intensity.get_value() assert det_counts == 9000.0 # 1 + 2 + 3 + 2 + 1 from detector = 9 assert mon_counts == 500.0 # 1k / 2k = 500 from monitor @@ -706,25 +703,25 @@ async def test_monitor_normalizer_det_sum_normal_mon_sum_wavelenth( # 1, 3 async def test_monitor_normalizer_det_sum_tof_bound_mon_sum_normal( # 2, 1 simpledae: SimpleDae, - monitor_normalizer_tof_bounded_zero_to_one_half_det_tof_mon_normal: MonitorNormalizer, + monitor_normalizer_zero_to_one_half_det_tof_mon_normal: MonitorNormalizer, spectra_bins_easy_to_test: sc.DataArray, ): - monitor_normalizer_tof_bounded_zero_to_one_half_det_tof_mon_normal.detectors[ + monitor_normalizer_zero_to_one_half_det_tof_mon_normal.detectors[ 1 ].read_spectrum_dataarray = AsyncMock(return_value=spectra_bins_easy_to_test) - monitor_normalizer_tof_bounded_zero_to_one_half_det_tof_mon_normal.monitors[ + monitor_normalizer_zero_to_one_half_det_tof_mon_normal.monitors[ 2 ].read_spectrum_dataarray = AsyncMock(return_value=spectra_bins_easy_to_test) - await monitor_normalizer_tof_bounded_zero_to_one_half_det_tof_mon_normal.reduce_data(simpledae) + await monitor_normalizer_zero_to_one_half_det_tof_mon_normal.reduce_data(simpledae) - det_counts = await monitor_normalizer_tof_bounded_zero_to_one_half_det_tof_mon_normal.det_counts.get_value() - mon_counts = await monitor_normalizer_tof_bounded_zero_to_one_half_det_tof_mon_normal.mon_counts.get_value() - intensity = await monitor_normalizer_tof_bounded_zero_to_one_half_det_tof_mon_normal.intensity.get_value() + det_counts = await monitor_normalizer_zero_to_one_half_det_tof_mon_normal.det_counts.get_value() + mon_counts = await monitor_normalizer_zero_to_one_half_det_tof_mon_normal.mon_counts.get_value() + intensity = await monitor_normalizer_zero_to_one_half_det_tof_mon_normal.intensity.get_value() - assert det_counts == 500 # 1 + 2 + 3 from detector = 6 - assert mon_counts == 9000.0 # 1 + 2 + 3 from monitor = 6 + assert det_counts == 500 # 1/2 * 1000 from detector = 500 + assert mon_counts == 9000.0 # 1 + 2 + 3 + 2 + 1 from monitor = 9000 assert intensity == pytest.approx(500 / 9000.0) @@ -822,7 +819,7 @@ async def test_monitor_normalizer_det_sum_tof_mon_sum_wavelength( # 2, 3 mon_counts = await monitor_normalizer_det_tof_mon_wavelenth.mon_counts.get_value() intensity = await monitor_normalizer_det_tof_mon_wavelenth.intensity.get_value() - assert det_counts == 500.0 # 1000 + (0.5 x 2000) from detector = 2000 + assert det_counts == 500.0 # 1/2 * 1000 from detector = 1000 assert mon_counts == pytest.approx(5675.097) # angstrom rebinning from monitor assert intensity == pytest.approx(500 / 5675.097) From 20d239d0deaa8d9b9b789db145fd1ad337bca286 Mon Sep 17 00:00:00 2001 From: esmithExperimentControls Date: Wed, 27 Nov 2024 11:29:08 +0000 Subject: [PATCH 13/25] Make class comments more concise and understandable. --- .../devices/simpledae/reducers.py | 53 ++++++++++++++----- 1 file changed, 39 insertions(+), 14 deletions(-) diff --git a/src/ibex_bluesky_core/devices/simpledae/reducers.py b/src/ibex_bluesky_core/devices/simpledae/reducers.py index 2436ea88..df3d28d3 100644 --- a/src/ibex_bluesky_core/devices/simpledae/reducers.py +++ b/src/ibex_bluesky_core/devices/simpledae/reducers.py @@ -33,6 +33,8 @@ async def sum_spectra(spectra: Collection[DaeSpectra]) -> sc.Variable | sc.DataA Returns a scipp scalar, which has .value and .variance properties for accessing the sum and variance respectively of the summed counts. + + More info on scipp scalars can be found here: https://scipp.github.io/generated/functions/scipp.scalar.html """ logger.info("Summing %d spectra using scipp", len(spectra)) summed_counts = sc.scalar(value=0, unit=sc.units.counts, dtype="float64") @@ -45,10 +47,17 @@ async def sum_spectra(spectra: Collection[DaeSpectra]) -> sc.Variable | sc.DataA def tof_bounded_spectra( bounds: sc.Variable, ) -> Callable[[Collection[DaeSpectra]], Awaitable[sc.Variable | sc.DataArray]]: - """Rebin spectra bounded by time of flight bounds. + """Sum a set of neutron spectra between the specified time of flight bounds. + + Args: + bounds: A scipp array of size 2, no variances, unit of us, + where the second element must be larger than the first. Returns a scipp scalar, which has .value and .variance properties for accessing the sum and variance respectively of the summed counts. + + More info on scipp arrays and scalars can be found here: https://scipp.github.io/generated/functions/scipp.scalar.html + """ if "tof" not in bounds.dims: raise ValueError("Should contain tof dims") @@ -69,10 +78,21 @@ async def sum_spectra_with_tof(spectra: Collection[DaeSpectra]) -> sc.Variable | def wavelength_bounded_spectra( bounds: sc.Variable, beam_total: sc.Variable ) -> Callable[[Collection[DaeSpectra]], Awaitable[sc.Variable | sc.DataArray]]: - """Rebin spectra bounded by wavelength bounds. + """Sum a set of neutron spectra between the specified [tof/wavelength] bounds. + + Args: + bounds: A scipp array of size 2 of wavelength bounds, in units of angstrom, + where the second element must be larger than the first. + beam_total: A scipp scalar of Ltotal (total flight path length), the path length + from neutron source to detector or monitor, in units of meters. + + Time of flight is converted to wavelength using scipp neutron's library function + `wavelength_from_tof`, more info on which can be found here: + https://scipp.github.io/scippneutron/generated/modules/scippneutron.conversion.tof.wavelength_from_tof.html Returns a scipp scalar, which has .value and .variance properties for accessing the sum and variance respectively of the summed counts. + """ if "tof" not in bounds.dims: raise ValueError("Should contain tof dims") @@ -101,7 +121,7 @@ def __init__( self, prefix: str, detector_spectra: Sequence[int], - summer: Callable[ + sum_detector: Callable[ [Collection[DaeSpectra]], Awaitable[sc.Variable | sc.DataArray] ] = sum_spectra, ) -> None: @@ -110,7 +130,8 @@ def __init__( Args: prefix: the PV prefix of the instrument to get spectra from (e.g. IN:DEMO:) detector_spectra: a sequence of spectra numbers (detectors) to sum. - summer: either sum_spectra, tof_bounded_spectra, or wavelength_bounded_spectra. + sum_detector: takes spectra objects, reads from them, and returns a scipp scalar + describing the detector intensity. Defaults to summing over the entire spectrum. """ self.detectors = DeviceVector( @@ -131,7 +152,7 @@ def __init__( self.intensity_stddev, self._intensity_stddev_setter = soft_signal_r_and_setter( float, 0.0, precision=INTENSITY_PRECISION ) - self.summer = summer + self.sum_detector = sum_detector super().__init__(name="") @@ -143,7 +164,7 @@ async def reduce_data(self, dae: "SimpleDae") -> None: """Apply the normalization.""" logger.info("starting reduction") summed_counts, denominator = await asyncio.gather( - self.summer(self.detectors.values()), self.denominator(dae).get_value() + self.sum_detector(self.detectors.values()), self.denominator(dae).get_value() ) self._det_counts_setter(float(summed_counts.value)) @@ -197,10 +218,10 @@ def __init__( prefix: str, detector_spectra: Sequence[int], monitor_spectra: Sequence[int], - detector_summer: Callable[ + sum_detector: Callable[ [Collection[DaeSpectra]], Awaitable[sc.Variable | sc.DataArray] ] = sum_spectra, - monitor_summer: Callable[ + sum_monitor: Callable[ [Collection[DaeSpectra]], Awaitable[sc.Variable | sc.DataArray] ] = sum_spectra, ) -> None: @@ -210,8 +231,12 @@ def __init__( prefix: the PV prefix of the instrument to get spectra from (e.g. IN:DEMO:) detector_spectra: a sequence of spectra numbers (detectors) to sum. monitor_spectra: a sequence of spectra number (monitors) to sum and normalize by. - detector_summer: either sum_spectra, tof_bounded_spectra, or wavelength_bounded_spectra. - monitor_summer: either sum_spectra, tof_bounded_spectra, or wavelength_bounded_spectra. + sum_detector: takes spectra objects, reads from them, and returns a scipp scalar + describing the detector intensity. Defaults to summing over the entire spectrum. + sum_monitor: takes spectra objects, reads from them, and returns a scipp scalar + describing the monitor intensity. Defaults to summing over the entire spectrum. + + Scipp scalars are described in further detail here: https://scipp.github.io/generated/functions/scipp.scalar.html """ dae_prefix = prefix + "DAE:" @@ -237,8 +262,8 @@ def __init__( self.intensity_stddev, self._intensity_stddev_setter = soft_signal_r_and_setter( float, 0.0, precision=INTENSITY_PRECISION ) - self.detector_summer = detector_summer - self.monitor_summer = monitor_summer + self.sum_detector = sum_detector + self.sum_monitor = sum_monitor super().__init__(name="") @@ -246,8 +271,8 @@ async def reduce_data(self, dae: "SimpleDae") -> None: """Apply the normalization.""" logger.info("starting reduction") detector_counts, monitor_counts = await asyncio.gather( - self.detector_summer(self.detectors.values()), - self.monitor_summer(self.monitors.values()), + self.sum_detector(self.detectors.values()), + self.sum_monitor(self.monitors.values()), ) if monitor_counts.value == 0.0: # To avoid zero division From 85c9b2e8f8d85b1b3e050847f8e472bb24e5706e Mon Sep 17 00:00:00 2001 From: esmithExperimentControls Date: Wed, 27 Nov 2024 11:29:47 +0000 Subject: [PATCH 14/25] Add empty line to create bulleted list in API reference docs --- src/ibex_bluesky_core/run_engine/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ibex_bluesky_core/run_engine/__init__.py b/src/ibex_bluesky_core/run_engine/__init__.py index f6d9344c..b2ec1417 100644 --- a/src/ibex_bluesky_core/run_engine/__init__.py +++ b/src/ibex_bluesky_core/run_engine/__init__.py @@ -72,6 +72,7 @@ def get_run_engine() -> RunEngine: RE.subscribe(lambda name, document: ...) For full documentation about the run engine, see: + - https://nsls-ii.github.io/bluesky/tutorial.html#the-runengine - https://nsls-ii.github.io/bluesky/run_engine_api.html """ From 849ab24cae380da30f06eba1221af3bf963f9381 Mon Sep 17 00:00:00 2001 From: esmithExperimentControls Date: Wed, 27 Nov 2024 11:32:24 +0000 Subject: [PATCH 15/25] Rename detector_summer and monitor_summer to sum_detector and sum_monitor --- tests/devices/simpledae/test_reducers.py | 38 +++++++++++++----------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/tests/devices/simpledae/test_reducers.py b/tests/devices/simpledae/test_reducers.py index 6d32a1c6..8c0d9a02 100644 --- a/tests/devices/simpledae/test_reducers.py +++ b/tests/devices/simpledae/test_reducers.py @@ -45,7 +45,7 @@ async def monitor_normalizer_zero_to_one_half_det_norm_mon_tof() -> MonitorNorma prefix="", detector_spectra=[1], monitor_spectra=[2], - monitor_summer=tof_bounded_spectra( + sum_monitor=tof_bounded_spectra( sc.array(dims=["tof"], values=[0.0, 0.5], unit=sc.units.us) ), ) @@ -60,7 +60,7 @@ async def monitor_normalizer_det_normal_mon_wavelenth() -> MonitorNormalizer: prefix="", detector_spectra=[1], monitor_spectra=[2], - monitor_summer=wavelength_bounded_spectra( + sum_monitor=wavelength_bounded_spectra( bounds=sc.array( dims=["tof"], values=[0.0, 5.1], unit=sc.units.angstrom, dtype="float64" ), @@ -78,7 +78,7 @@ async def monitor_normalizer_zero_to_one_half_det_tof_mon_normal() -> MonitorNor prefix="", detector_spectra=[1], monitor_spectra=[2], - detector_summer=tof_bounded_spectra( + sum_detector=tof_bounded_spectra( sc.array(dims=["tof"], values=[0.0, 0.5], unit=sc.units.us) ), ) @@ -93,10 +93,10 @@ async def monitor_normalizer_tof_bounded_one_to_three() -> MonitorNormalizer: prefix="", detector_spectra=[1], monitor_spectra=[2], - detector_summer=tof_bounded_spectra( + sum_detector=tof_bounded_spectra( sc.array(dims=["tof"], values=[0.0, 3.0], unit=sc.units.us) ), - monitor_summer=tof_bounded_spectra( + sum_monitor=tof_bounded_spectra( sc.array(dims=["tof"], values=[0.0, 3.0], unit=sc.units.us) ), ) @@ -110,10 +110,10 @@ async def monitor_normalizer_tof_bounded_zero_to_one_half() -> MonitorNormalizer prefix="", detector_spectra=[1], monitor_spectra=[2], - detector_summer=tof_bounded_spectra( + sum_detector=tof_bounded_spectra( sc.array(dims=["tof"], values=[0.0, 0.5], unit=sc.units.us) ), - monitor_summer=tof_bounded_spectra( + sum_monitor=tof_bounded_spectra( sc.array(dims=["tof"], values=[0.0, 0.5], unit=sc.units.us) ), ) @@ -128,10 +128,10 @@ async def monitor_normalizer_det_tof_mon_wavelenth() -> MonitorNormalizer: prefix="", detector_spectra=[1], monitor_spectra=[2], - detector_summer=tof_bounded_spectra( + sum_detector=tof_bounded_spectra( sc.array(dims=["tof"], values=[0.0, 0.5], unit=sc.units.us) ), - monitor_summer=wavelength_bounded_spectra( + sum_monitor=wavelength_bounded_spectra( bounds=sc.array( dims=["tof"], values=[0.0, 5.1], unit=sc.units.angstrom, dtype="float64" ), @@ -149,7 +149,7 @@ async def monitor_normalizer_det_wavelength_mon_normal() -> MonitorNormalizer: prefix="", detector_spectra=[1], monitor_spectra=[2], - detector_summer=wavelength_bounded_spectra( + sum_detector=wavelength_bounded_spectra( bounds=sc.array( dims=["tof"], values=[0.0, 5.1], unit=sc.units.angstrom, dtype="float64" ), @@ -167,13 +167,13 @@ async def monitor_normalizer_det_wavelength_mon_tof() -> MonitorNormalizer: prefix="", detector_spectra=[1], monitor_spectra=[2], - detector_summer=wavelength_bounded_spectra( + sum_detector=wavelength_bounded_spectra( bounds=sc.array( dims=["tof"], values=[0.0, 5.1], unit=sc.units.angstrom, dtype="float64" ), beam_total=sc.scalar(value=10.0, unit=sc.units.m), ), - monitor_summer=tof_bounded_spectra( + sum_monitor=tof_bounded_spectra( sc.array(dims=["tof"], values=[0.0, 0.5], unit=sc.units.us) ), ) @@ -188,13 +188,13 @@ async def monitor_normalizer_det_wavelength_mon_wavelength() -> MonitorNormalize prefix="", detector_spectra=[1], monitor_spectra=[2], - detector_summer=wavelength_bounded_spectra( + sum_detector=wavelength_bounded_spectra( bounds=sc.array( dims=["tof"], values=[0.0, 5.1], unit=sc.units.angstrom, dtype="float64" ), beam_total=sc.scalar(value=10.0, unit=sc.units.m), ), - monitor_summer=wavelength_bounded_spectra( + sum_monitor=wavelength_bounded_spectra( bounds=sc.array( dims=["tof"], values=[0.0, 4.5], unit=sc.units.angstrom, dtype="float64" ), @@ -211,7 +211,7 @@ async def scalar_normalizer_bounded_sum_zero_to_one_half(simpledae: SimpleDae) - reducer = PeriodGoodFramesNormalizer( prefix="", detector_spectra=[1, 2], - summer=tof_bounded_spectra(sc.array(dims=["tof"], values=[0, 0.5], unit=sc.units.us)), + sum_detector=tof_bounded_spectra(sc.array(dims=["tof"], values=[0, 0.5], unit=sc.units.us)), ) await reducer.connect(mock=True) return reducer @@ -223,7 +223,9 @@ async def scalar_normalizer_bounded_sum_one_to_one(simpledae: SimpleDae) -> Scal reducer = PeriodGoodFramesNormalizer( prefix="", detector_spectra=[1, 2], - summer=tof_bounded_spectra(sc.array(dims=["tof"], values=[1.0, 1.0], unit=sc.units.us)), + sum_detector=tof_bounded_spectra( + sc.array(dims=["tof"], values=[1.0, 1.0], unit=sc.units.us) + ), ) await reducer.connect(mock=True) return reducer @@ -237,7 +239,9 @@ async def scalar_normalizer_bounded_sum_one_to_three_micro_sec( reducer = PeriodGoodFramesNormalizer( prefix="", detector_spectra=[1, 2], - summer=tof_bounded_spectra(sc.array(dims=["tof"], values=[0.0, 3.0], unit=sc.units.us)), + sum_detector=tof_bounded_spectra( + sc.array(dims=["tof"], values=[0.0, 3.0], unit=sc.units.us) + ), ) await reducer.connect(mock=True) return reducer From 4e97f7fdedd69cff6697aa19ce14631317d9b339 Mon Sep 17 00:00:00 2001 From: esmithExperimentControls Date: Wed, 27 Nov 2024 11:32:53 +0000 Subject: [PATCH 16/25] documentation on time of flight and wavelength bounding spectra. --- doc/devices/dae.md | 63 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/doc/devices/dae.md b/doc/devices/dae.md index 9a660a29..cfc5fbcf 100644 --- a/doc/devices/dae.md +++ b/doc/devices/dae.md @@ -205,6 +205,54 @@ Published signals: - `reducer.mon_counts_stddev` - uncertainty (standard deviation) of the summed monitor counts - `reducer.intensity_stddev` - uncertainty (standard deviation) of the normalised intensity +### Time of Flight and Wavelength Bounding Spectra + +Scalar Normalizers (such as PeriodGoodFramesNormalizer, GoodFramesNormalizer) can be passed a +summing function which can optionally sum spectra between provided time of flight or wavelength bounds. + + +Here is an example showing creating a scalar normalizer with time of flight bounds from 15000 to 25000 μs, and summing 2 detectors: +``` +import scipp + +bounds=scipp.array(dims=["tof"], values=[15000.0, 25000.0], unit=scipp.units.us) + +reducer = PeriodGoodFramesNormalizer( + prefix=get_pv_prefix(), + detector_spectra=[1, 2], + summer=tof_bounded_spectra(bounds) +) +``` + +{py:obj}`ibex_bluesky_core.devices.simpledae.reducers.tof_bounded_spectra` + + +Monitor Normalizers, which have both a monitor as well as detector, can be passed a summing function for each of these components independently, e.g. the detector can use time of flight while the monitor uses wavelength. Here is an example with wavelength bounding used to sum the monitor component, and time of flight bounding for the detector summing spectra: + +``` +import scipp + +wavelength_bounds = scipp.array(dims=["tof"], values=[0.0, 5.1], unit=scipp.units.angstrom, dtype="float64") +beam_total = scipp.scalar(value=85.0, unit=sc.units.m), +tof_bounds = scipp.array(dims=["tof"], values=[15000, 25000], unit=scipp.units.us) + +reducer = MonitorNormalizer( + prefix=get_pv_prefix(), + detector_spectra=[1], + monitor_spectra=[2], + detector_summer=wavelength_bounded_spectra(wavelength_bounds, beam_total), + monitor_summer=tof_bounded_spectra(tof_bounds) +) +``` +{py:obj}`ibex_bluesky_core.devices.simpledae.reducers.wavelength_bounded_spectra` + + +- In either case, the bounds are passed as a scipp array, which needs a `dims` attribute, `values` passed +as a list, and `units` (μs/microseconds for time of flight bounding, and angstrom for wavelength bounding) + +- If you don't specify either of these options, they will default to an unbounded spectra + + ## Waiters A `waiter` defines an arbitrary strategy for how long to count at each point. @@ -362,3 +410,18 @@ A `DaeSpectrum` object provides 3 arrays: The `Dae` base class does not provide any spectra by default. User-level classes should specify the set of spectra which they are interested in. + + + + +Spectra can be rebinned based on time of flight bounds, or wavelength bounds, for both detector +and monitor normalizers. + +Both Scalar Normalizers (PeriodGoodFramesNormalizer, GoodFramesNormalizer) and MonitorNormalizers +provide the following: +- `detector_summer`: sums counts using pre-existing bins, or rebins using time of flight bounds, +or wavelength bounds. +- `monitor_summer` (MonitorNormalizer only): sums counts using sums counts using pre-existing bins, +or rebins using time of flight bounds, or wavelength bounds. + +For both options, the default, if none is specified, is to use pre-existing bins. \ No newline at end of file From b4dd536289f70e42b123967e73b5dd26851b61d6 Mon Sep 17 00:00:00 2001 From: esmithExperimentControls Date: Wed, 27 Nov 2024 11:43:44 +0000 Subject: [PATCH 17/25] Removed trailing whitespace --- src/ibex_bluesky_core/run_engine/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ibex_bluesky_core/run_engine/__init__.py b/src/ibex_bluesky_core/run_engine/__init__.py index b2ec1417..bafe1c36 100644 --- a/src/ibex_bluesky_core/run_engine/__init__.py +++ b/src/ibex_bluesky_core/run_engine/__init__.py @@ -72,7 +72,7 @@ def get_run_engine() -> RunEngine: RE.subscribe(lambda name, document: ...) For full documentation about the run engine, see: - + - https://nsls-ii.github.io/bluesky/tutorial.html#the-runengine - https://nsls-ii.github.io/bluesky/run_engine_api.html """ From 35906a76ba3fbbb3249d3c83688b3f2fcb471757 Mon Sep 17 00:00:00 2001 From: esmithExperimentControls Date: Thu, 12 Dec 2024 14:38:22 +0000 Subject: [PATCH 18/25] Some rewording fixes, and adding more API references. --- doc/devices/dae.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/doc/devices/dae.md b/doc/devices/dae.md index cfc5fbcf..371f2652 100644 --- a/doc/devices/dae.md +++ b/doc/devices/dae.md @@ -210,6 +210,8 @@ Published signals: Scalar Normalizers (such as PeriodGoodFramesNormalizer, GoodFramesNormalizer) can be passed a summing function which can optionally sum spectra between provided time of flight or wavelength bounds. +{py:obj}`ibex_bluesky_core.some.path.PeriodGoodFramesNormalizer` + Here is an example showing creating a scalar normalizer with time of flight bounds from 15000 to 25000 μs, and summing 2 detectors: ``` @@ -233,14 +235,14 @@ Monitor Normalizers, which have both a monitor as well as detector, can be passe import scipp wavelength_bounds = scipp.array(dims=["tof"], values=[0.0, 5.1], unit=scipp.units.angstrom, dtype="float64") -beam_total = scipp.scalar(value=85.0, unit=sc.units.m), +total_flight_path_length = scipp.scalar(value=85.0, unit=sc.units.m), tof_bounds = scipp.array(dims=["tof"], values=[15000, 25000], unit=scipp.units.us) reducer = MonitorNormalizer( prefix=get_pv_prefix(), detector_spectra=[1], monitor_spectra=[2], - detector_summer=wavelength_bounded_spectra(wavelength_bounds, beam_total), + detector_summer=wavelength_bounded_spectra(wavelength_bounds, total_flight_path_length), monitor_summer=tof_bounded_spectra(tof_bounds) ) ``` @@ -250,7 +252,7 @@ reducer = MonitorNormalizer( - In either case, the bounds are passed as a scipp array, which needs a `dims` attribute, `values` passed as a list, and `units` (μs/microseconds for time of flight bounding, and angstrom for wavelength bounding) -- If you don't specify either of these options, they will default to an unbounded spectra +- If you don't specify either of these options, they will default to an summing over the entire spectrum. ## Waiters @@ -414,14 +416,13 @@ the set of spectra which they are interested in. -Spectra can be rebinned based on time of flight bounds, or wavelength bounds, for both detector -and monitor normalizers. +Spectra can be summed between two bounds based on time of flight bounds, or wavelength bounds, for both detector and monitor normalizers. Both Scalar Normalizers (PeriodGoodFramesNormalizer, GoodFramesNormalizer) and MonitorNormalizers -provide the following: -- `detector_summer`: sums counts using pre-existing bins, or rebins using time of flight bounds, +accept the following arguments: +- `detector_summer`: sums counts using pre-existing bounds, or sums using time of flight bounds, or wavelength bounds. -- `monitor_summer` (MonitorNormalizer only): sums counts using sums counts using pre-existing bins, -or rebins using time of flight bounds, or wavelength bounds. +- `monitor_summer` (MonitorNormalizer only): sums counts using pre-existing bounds, +or sums using time of flight bounds, or wavelength bounds. -For both options, the default, if none is specified, is to use pre-existing bins. \ No newline at end of file +For both options, the default, if none is specified, is to use pre-existing bounds. \ No newline at end of file From 62e90a8865529c1c64941516417174167a927c93 Mon Sep 17 00:00:00 2001 From: esmithExperimentControls Date: Fri, 13 Dec 2024 09:48:40 +0000 Subject: [PATCH 19/25] rename beam_total to total_flight_path_length, add test for wavelength bounds not equal to 2. --- tests/devices/simpledae/test_reducers.py | 38 +++++++++++++++--------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/tests/devices/simpledae/test_reducers.py b/tests/devices/simpledae/test_reducers.py index 43300aaf..eb810b57 100644 --- a/tests/devices/simpledae/test_reducers.py +++ b/tests/devices/simpledae/test_reducers.py @@ -64,7 +64,7 @@ async def monitor_normalizer_det_normal_mon_wavelenth() -> MonitorNormalizer: bounds=sc.array( dims=["tof"], values=[0.0, 5.1], unit=sc.units.angstrom, dtype="float64" ), - beam_total=sc.scalar(value=10.0, unit=sc.units.m), + total_flight_path_length=sc.scalar(value=10.0, unit=sc.units.m), ), ) await reducer.connect(mock=True) @@ -135,7 +135,7 @@ async def monitor_normalizer_det_tof_mon_wavelenth() -> MonitorNormalizer: bounds=sc.array( dims=["tof"], values=[0.0, 5.1], unit=sc.units.angstrom, dtype="float64" ), - beam_total=sc.scalar(value=10.0, unit=sc.units.m), + total_flight_path_length=sc.scalar(value=10.0, unit=sc.units.m), ), ) await reducer.connect(mock=True) @@ -153,7 +153,7 @@ async def monitor_normalizer_det_wavelength_mon_normal() -> MonitorNormalizer: bounds=sc.array( dims=["tof"], values=[0.0, 5.1], unit=sc.units.angstrom, dtype="float64" ), - beam_total=sc.scalar(value=10.0, unit=sc.units.m), + total_flight_path_length=sc.scalar(value=10.0, unit=sc.units.m), ), ) await reducer.connect(mock=True) @@ -171,7 +171,7 @@ async def monitor_normalizer_det_wavelength_mon_tof() -> MonitorNormalizer: bounds=sc.array( dims=["tof"], values=[0.0, 5.1], unit=sc.units.angstrom, dtype="float64" ), - beam_total=sc.scalar(value=10.0, unit=sc.units.m), + total_flight_path_length=sc.scalar(value=10.0, unit=sc.units.m), ), sum_monitor=tof_bounded_spectra( sc.array(dims=["tof"], values=[0.0, 0.5], unit=sc.units.us) @@ -192,13 +192,13 @@ async def monitor_normalizer_det_wavelength_mon_wavelength() -> MonitorNormalize bounds=sc.array( dims=["tof"], values=[0.0, 5.1], unit=sc.units.angstrom, dtype="float64" ), - beam_total=sc.scalar(value=10.0, unit=sc.units.m), + total_flight_path_length=sc.scalar(value=10.0, unit=sc.units.m), ), sum_monitor=wavelength_bounded_spectra( bounds=sc.array( dims=["tof"], values=[0.0, 4.5], unit=sc.units.angstrom, dtype="float64" ), - beam_total=sc.scalar(value=10.0, unit=sc.units.m), + total_flight_path_length=sc.scalar(value=10.0, unit=sc.units.m), ), ) await reducer.connect(mock=True) @@ -248,7 +248,7 @@ async def scalar_normalizer_bounded_sum_one_to_three_micro_sec( @pytest.fixture -async def spectra_bins_easy_to_test() -> sc.DataArray: +def spectra_bins_easy_to_test() -> sc.DataArray: return sc.DataArray( data=sc.Variable( dims=["tof"], @@ -265,7 +265,7 @@ async def spectra_bins_easy_to_test() -> sc.DataArray: @pytest.fixture -async def spectra_bins_tof_convert_to_wavelength_easy() -> sc.DataArray: +def spectra_bins_tof_convert_to_wavelength_easy() -> sc.DataArray: return sc.DataArray( data=sc.Variable( dims=["tof"], @@ -285,7 +285,7 @@ async def spectra_bins_tof_convert_to_wavelength_easy() -> sc.DataArray: @pytest.fixture -async def spectra_bins_tof_convert_to_wavelength_easy_copy() -> sc.DataArray: +def spectra_bins_tof_convert_to_wavelength_easy_copy() -> sc.DataArray: return sc.DataArray( data=sc.Variable( dims=["tof"], @@ -675,7 +675,7 @@ async def test_monitor_normalizer_det_sum_normal_mon_sum_tof_bound_( # 1, 2 intensity = await monitor_normalizer_zero_to_one_half_det_norm_mon_tof.intensity.get_value() assert det_counts == 9000.0 # 1 + 2 + 3 + 2 + 1 from detector = 9 - assert mon_counts == 500.0 # 1k / 2k = 500 from monitor + assert mon_counts == 500.0 # 1k / 2 = 500 from monitor assert intensity == pytest.approx(9000.0 / 500.0) @@ -799,7 +799,7 @@ async def test_monitor_normalizer_det_mon_summer_tof_bounded_zero_to_one_half( assert det_counts == 500 # 500 from first detector assert mon_counts == 2000 # 2000 half of monitor first bin - assert intensity == pytest.approx(0.25) # 500 / 2500 + assert intensity == pytest.approx(0.25) # 500 / 2000 # test_monitor_normalizer_det_sum_tof_bound_mon_sum_wavelength 2, 3 @@ -915,12 +915,22 @@ def test_tof_bounded_spectra_bounds_missing_or_too_many(data: list[float]): def test_tof_bounded_spectra_missing_tof_in_bounds_dims(): - with pytest.raises(expected_exception=ValueError): + with pytest.raises(expected_exception=ValueError, match="Should contain tof dims"): tof_bounded_spectra(sc.array(dims=[""], values=[0], unit=sc.units.us)) def test_wavelength_bounded_spectra_missing_tof_in_bounds_dims(): - with pytest.raises(expected_exception=ValueError): + with pytest.raises(expected_exception=ValueError, match="Should contain tof dims"): wavelength_bounded_spectra( - bounds=sc.array(dims=[""], values=[0], unit=sc.units.us), beam_total=sc.scalar(value=0) + bounds=sc.array(dims=[""], values=[0], unit=sc.units.us), + total_flight_path_length=sc.scalar(value=0), + ) + + +@pytest.mark.parametrize("data", [[], [0.0], [0.1, 0.2, 0.3]]) +def test_wavelength_bounded_spectra_bounds_missing_or_too_many(data: list[float]): + with pytest.raises(expected_exception=ValueError, match="Should contain lower and upper bound"): + wavelength_bounded_spectra( + bounds=sc.array(dims=["tof"], values=data, unit=sc.units.us), + total_flight_path_length=sc.scalar(value=100, unit=sc.units.m), ) From 9294e97d60f3704a09031895fcd86fd9cda2df8f Mon Sep 17 00:00:00 2001 From: esmithExperimentControls Date: Fri, 13 Dec 2024 09:59:19 +0000 Subject: [PATCH 20/25] merge changes with imports, add local variable bounds_value to compare number of bounds, and rephrasing some API reference explanation. --- .../devices/simpledae/reducers.py | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/ibex_bluesky_core/devices/simpledae/reducers.py b/src/ibex_bluesky_core/devices/simpledae/reducers.py index 86eeb567..db69f3eb 100644 --- a/src/ibex_bluesky_core/devices/simpledae/reducers.py +++ b/src/ibex_bluesky_core/devices/simpledae/reducers.py @@ -3,9 +3,9 @@ import asyncio import logging import math -from abc import ABCMeta, abstractmethod +from abc import ABC, abstractmethod from collections.abc import Collection, Sequence -from typing import TYPE_CHECKING, Awaitable, Callable, Collection, Sequence +from typing import TYPE_CHECKING, Awaitable, Callable import scipp as sc from ophyd_async.core import ( @@ -60,9 +60,10 @@ def tof_bounded_spectra( More info on scipp arrays and scalars can be found here: https://scipp.github.io/generated/functions/scipp.scalar.html """ + bounds_value = 2 if "tof" not in bounds.dims: raise ValueError("Should contain tof dims") - if bounds.sizes["tof"] != 2: + if bounds.sizes["tof"] != bounds_value: raise ValueError("Should contain lower and upper bound") async def sum_spectra_with_tof(spectra: Collection[DaeSpectra]) -> sc.Variable | sc.DataArray: @@ -77,15 +78,15 @@ async def sum_spectra_with_tof(spectra: Collection[DaeSpectra]) -> sc.Variable | def wavelength_bounded_spectra( - bounds: sc.Variable, beam_total: sc.Variable + bounds: sc.Variable, total_flight_path_length: sc.Variable ) -> Callable[[Collection[DaeSpectra]], Awaitable[sc.Variable | sc.DataArray]]: - """Sum a set of neutron spectra between the specified [tof/wavelength] bounds. + """Sum a set of neutron spectra between the specified wavelength bounds. Args: bounds: A scipp array of size 2 of wavelength bounds, in units of angstrom, where the second element must be larger than the first. - beam_total: A scipp scalar of Ltotal (total flight path length), the path length - from neutron source to detector or monitor, in units of meters. + total_flight_path_length: A scipp scalar of Ltotal (total flight path length), the path + length from neutron source to detector or monitor, in units of meters. Time of flight is converted to wavelength using scipp neutron's library function `wavelength_from_tof`, more info on which can be found here: @@ -95,18 +96,22 @@ def wavelength_bounded_spectra( and variance respectively of the summed counts. """ + bounds_value = 2 + if "tof" not in bounds.dims: raise ValueError("Should contain tof dims") + if bounds.sizes["tof"] != bounds_value: + raise ValueError("Should contain lower and upper bound") async def sum_spectra_with_wavelength( spectra: Collection[DaeSpectra], ) -> sc.Variable | sc.DataArray: - """Sum spectra and convert time of flight to wavelength.""" + """Sum a set of spectra between the specified wavelength bounds.""" summed_counts = sc.scalar(value=0, unit=sc.units.counts, dtype="float64") for spec in asyncio.as_completed([s.read_spectrum_dataarray() for s in spectra]): wavelength_bounded_spectra = await spec wavelength_coord = conversion.tof.wavelength_from_tof( - tof=wavelength_bounded_spectra.coords["tof"], Ltotal=beam_total + tof=wavelength_bounded_spectra.coords["tof"], Ltotal=total_flight_path_length ) wavelength_bounded_spectra.coords["tof"] = wavelength_coord summed_counts += wavelength_bounded_spectra.rebin({"tof": bounds}).sum() @@ -115,7 +120,7 @@ async def sum_spectra_with_wavelength( return sum_spectra_with_wavelength -class ScalarNormalizer(Reducer, StandardReadable, metaclass=ABCMeta): +class ScalarNormalizer(Reducer, StandardReadable, ABC): """Sum a set of user-specified spectra, then normalize by a scalar signal.""" def __init__( From 15347997c17536c69f67cb6f52619f9be1b0666d Mon Sep 17 00:00:00 2001 From: esmithExperimentControls Date: Fri, 13 Dec 2024 10:30:46 +0000 Subject: [PATCH 21/25] ruff change import Awaitable from collections.abc --- src/ibex_bluesky_core/devices/simpledae/reducers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ibex_bluesky_core/devices/simpledae/reducers.py b/src/ibex_bluesky_core/devices/simpledae/reducers.py index db69f3eb..a04f8821 100644 --- a/src/ibex_bluesky_core/devices/simpledae/reducers.py +++ b/src/ibex_bluesky_core/devices/simpledae/reducers.py @@ -4,8 +4,8 @@ import logging import math from abc import ABC, abstractmethod -from collections.abc import Collection, Sequence -from typing import TYPE_CHECKING, Awaitable, Callable +from collections.abc import Awaitable, Collection, Sequence +from typing import TYPE_CHECKING, Callable import scipp as sc from ophyd_async.core import ( From 1fa1c9caaabcc1e85419da0e600773250e7e1b5b Mon Sep 17 00:00:00 2001 From: esmithExperimentControls Date: Fri, 13 Dec 2024 10:57:45 +0000 Subject: [PATCH 22/25] Fix pyobj path reference for PeriodGoodFramesNormalizer --- doc/devices/dae.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/devices/dae.md b/doc/devices/dae.md index 371f2652..a6a4798f 100644 --- a/doc/devices/dae.md +++ b/doc/devices/dae.md @@ -210,7 +210,7 @@ Published signals: Scalar Normalizers (such as PeriodGoodFramesNormalizer, GoodFramesNormalizer) can be passed a summing function which can optionally sum spectra between provided time of flight or wavelength bounds. -{py:obj}`ibex_bluesky_core.some.path.PeriodGoodFramesNormalizer` +{py:obj}`ibex_bluesky_core.src.ibex_bluesky_core.devices.simpledae.reducers.PeriodGoodFramesNormalizer` Here is an example showing creating a scalar normalizer with time of flight bounds from 15000 to 25000 μs, and summing 2 detectors: From 34d9b58ea5c01aef24a53482e2c8ee0d9580c68b Mon Sep 17 00:00:00 2001 From: esmithExperimentControls Date: Fri, 13 Dec 2024 11:02:02 +0000 Subject: [PATCH 23/25] fix path for py object --- doc/devices/dae.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/devices/dae.md b/doc/devices/dae.md index a6a4798f..29066a92 100644 --- a/doc/devices/dae.md +++ b/doc/devices/dae.md @@ -210,7 +210,7 @@ Published signals: Scalar Normalizers (such as PeriodGoodFramesNormalizer, GoodFramesNormalizer) can be passed a summing function which can optionally sum spectra between provided time of flight or wavelength bounds. -{py:obj}`ibex_bluesky_core.src.ibex_bluesky_core.devices.simpledae.reducers.PeriodGoodFramesNormalizer` +{py:obj}`ibex_bluesky_core..devices.simpledae.reducers.PeriodGoodFramesNormalizer` Here is an example showing creating a scalar normalizer with time of flight bounds from 15000 to 25000 μs, and summing 2 detectors: From 190e38390acb76713553c6309bd2a7c0b702a1ae Mon Sep 17 00:00:00 2001 From: esmithExperimentControls Date: Fri, 13 Dec 2024 11:02:33 +0000 Subject: [PATCH 24/25] fix py obj path --- doc/devices/dae.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/devices/dae.md b/doc/devices/dae.md index 29066a92..e4209d0d 100644 --- a/doc/devices/dae.md +++ b/doc/devices/dae.md @@ -210,7 +210,7 @@ Published signals: Scalar Normalizers (such as PeriodGoodFramesNormalizer, GoodFramesNormalizer) can be passed a summing function which can optionally sum spectra between provided time of flight or wavelength bounds. -{py:obj}`ibex_bluesky_core..devices.simpledae.reducers.PeriodGoodFramesNormalizer` +{py:obj}`ibex_bluesky_core.devices.simpledae.reducers.PeriodGoodFramesNormalizer` Here is an example showing creating a scalar normalizer with time of flight bounds from 15000 to 25000 μs, and summing 2 detectors: From f02a0148169b1ac9b5efa06b8fb4a064d81568a8 Mon Sep 17 00:00:00 2001 From: esmithExperimentControls Date: Mon, 16 Dec 2024 09:19:50 +0000 Subject: [PATCH 25/25] Add explanation about assuming pixels share the same flight path length --- doc/devices/dae.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/devices/dae.md b/doc/devices/dae.md index e4209d0d..80142844 100644 --- a/doc/devices/dae.md +++ b/doc/devices/dae.md @@ -229,7 +229,9 @@ reducer = PeriodGoodFramesNormalizer( {py:obj}`ibex_bluesky_core.devices.simpledae.reducers.tof_bounded_spectra` -Monitor Normalizers, which have both a monitor as well as detector, can be passed a summing function for each of these components independently, e.g. the detector can use time of flight while the monitor uses wavelength. Here is an example with wavelength bounding used to sum the monitor component, and time of flight bounding for the detector summing spectra: +Monitor Normalizers, which have both a monitor as well as detector, can be passed a summing function for each of these components independently, e.g. the detector can use time of flight while the monitor uses wavelength. tof_bounded_spectra assumes that all pixels being summed share the same flight-path length. Where two separate instances of tof_bounded_spectra are used, such as in DetectorMonitorNormalizer, these may have different flight path lengths from each other. + +Here is an example with wavelength bounding used to sum the monitor component, and time of flight bounding for the detector summing spectra: ``` import scipp