From a088286957a4b3f05273b7c04a2cd3b4082412c4 Mon Sep 17 00:00:00 2001 From: Jack Doughty Date: Tue, 8 Oct 2024 14:17:40 +0100 Subject: [PATCH 01/17] Generate individual & summed uncertainties --- .../devices/dae/dae_spectra.py | 9 +- .../devices/simpledae/reducers.py | 69 +++++++++++- tests/devices/simpledae/test_reducers.py | 104 ++++++++++++++++++ 3 files changed, 180 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 0c90469..3378fc3 100644 --- a/src/ibex_bluesky_core/devices/dae/dae_spectra.py +++ b/src/ibex_bluesky_core/devices/dae/dae_spectra.py @@ -6,7 +6,7 @@ from event_model.documents.event_descriptor import DataKey from numpy import float32 from numpy.typing import NDArray -from ophyd_async.core import SignalR, StandardReadable +from ophyd_async.core import SignalR, StandardReadable, soft_signal_r_and_setter from ophyd_async.epics.signal import epics_signal_r @@ -56,6 +56,8 @@ def __init__(self, dae_prefix: str, *, spectra: int, period: int, name: str = "" int, f"{dae_prefix}SPEC:{period}:{spectra}:YC.NORD" ) + self.stddev, self._stddev_setter = soft_signal_r_and_setter(NDArray[float32], [0.0]) + super().__init__(name=name) async def _read_sized( @@ -79,6 +81,11 @@ async def read_counts(self) -> NDArray[float32]: async def read_counts_per_time(self) -> NDArray[float32]: """Read a correctly-sized array of counts divided by bin width.""" return await self._read_sized(self.counts_per_time, self.counts_per_time_size) + + async def read_counts_uncertainties(self) -> NDArray[float32]: + """Read a correctly-sized array of uncertainties for each count.""" + + return await self._read_sized(self.stddev, self.counts_size) # type: ignore async def read_spectrum_dataarray(self) -> sc.DataArray: """Get a scipp DataArray containing the current data from this spectrum. diff --git a/src/ibex_bluesky_core/devices/simpledae/reducers.py b/src/ibex_bluesky_core/devices/simpledae/reducers.py index 067b32b..77568fe 100644 --- a/src/ibex_bluesky_core/devices/simpledae/reducers.py +++ b/src/ibex_bluesky_core/devices/simpledae/reducers.py @@ -2,9 +2,11 @@ import asyncio from abc import ABCMeta, abstractmethod -from typing import TYPE_CHECKING, Collection, Sequence +from typing import TYPE_CHECKING, Collection, Sequence, Tuple +import numpy as np import scipp as sc +import math from ophyd_async.core import ( Device, DeviceVector, @@ -53,6 +55,9 @@ def __init__(self, prefix: str, detector_spectra: Sequence[int]) -> None: self.det_counts, self._det_counts_setter = soft_signal_r_and_setter(float, 0.0) self.intensity, self._intensity_setter = soft_signal_r_and_setter(float, 0.0, precision=6) + self.det_counts_stddev, self._det_counts_stddev_setter = soft_signal_r_and_setter(float, 0.0) + self.intensity_stddev, self._intensity_stddev_setter = soft_signal_r_and_setter(float, 0.0, precision=6) + super().__init__(name="") @abstractmethod @@ -68,6 +73,18 @@ async def reduce_data(self, dae: "SimpleDae") -> None: self._det_counts_setter(float(summed_counts.value)) self._intensity_setter(float(summed_counts.value) / denominator) + detector_counts_var = 0.0 if summed_counts.variance is None else summed_counts.variance + + var_temp = (summed_counts / denominator).variance + intensity_var = var_temp if var_temp is not None else 0.0 + + self._det_counts_stddev_setter(math.sqrt(detector_counts_var)) + self._intensity_stddev_setter(math.sqrt(intensity_var)) + + for spec in self.detectors.values(): + stddev = await spec.read_counts() + spec._stddev_setter(np.sqrt(stddev)) + def additional_readable_signals(self, dae: "SimpleDae") -> list[Device]: """Publish interesting signals derived or used by this reducer.""" return [ @@ -75,6 +92,18 @@ def additional_readable_signals(self, dae: "SimpleDae") -> list[Device]: self.intensity, self.denominator(dae), ] + + def additional_readable_signals_uncertainties(self, dae: "SimpleDae") -> list[Device]: + """Publish interesting signals derived or used by this reducer, including respective uncertainties.""" + return [ + self.det_counts_stddev, + self.intensity_stddev, + ] + + def readable_detector_count_uncertainties(self, dae: "SimpleDae") -> list[Device]: + """Publish individual uncertainty signals for all detectors.""" + + return [self.detectors[i].stddev for i in self.detectors] class PeriodGoodFramesNormalizer(ScalarNormalizer): @@ -119,6 +148,10 @@ def __init__( self.mon_counts, self._mon_counts_setter = soft_signal_r_and_setter(float, 0.0) self.intensity, self._intensity_setter = soft_signal_r_and_setter(float, 0.0, precision=6) + self.det_counts_stddev, self._det_counts_stddev_setter = soft_signal_r_and_setter(float, 0.0) + self.mon_counts_stddev, self._mon_counts_stddev_setter = soft_signal_r_and_setter(float, 0.0) + self.intensity_stddev, self._intensity_stddev_setter = soft_signal_r_and_setter(float, 0.0, precision=6) + super().__init__(name="") async def reduce_data(self, dae: "SimpleDae") -> None: @@ -131,6 +164,22 @@ async def reduce_data(self, dae: "SimpleDae") -> None: self._mon_counts_setter(float(monitor_counts.value)) self._intensity_setter(float((detector_counts / monitor_counts).value)) + detector_counts_var = 0.0 if detector_counts.variance is None else detector_counts.variance + monitor_counts_var = 0.0 if monitor_counts.variance is None else monitor_counts.variance + intensity_var = 0.0 if monitor_counts_var == 0.0 else (detector_counts / monitor_counts).variance + + self._det_counts_stddev_setter(math.sqrt(detector_counts_var)) + self._mon_counts_stddev_setter(math.sqrt(monitor_counts_var)) + self._intensity_stddev_setter(math.sqrt(intensity_var)) + + for spec in self.detectors.values(): + stddev = await spec.read_counts() + spec._stddev_setter(np.sqrt(stddev)) + + for spec in self.monitors.values(): + stddev = await spec.read_counts() + spec._stddev_setter(np.sqrt(stddev)) + def additional_readable_signals(self, dae: "SimpleDae") -> list[Device]: """Publish interesting signals derived or used by this reducer.""" return [ @@ -138,3 +187,21 @@ def additional_readable_signals(self, dae: "SimpleDae") -> list[Device]: self.mon_counts, self.intensity, ] + + def additional_readable_signals_uncertainties(self, dae: "SimpleDae") -> list[Device]: + """Publish interesting signals derived or used by this reducer, including respective uncertainties.""" + return [ + self.det_counts_stddev, + self.mon_counts_stddev, + self.intensity_stddev, + ] + + def readable_detector_count_uncertainties(self, dae: "SimpleDae") -> list[Device]: + """Publish individual uncertainty signals for all detectors.""" + + return [self.detectors[i].stddev for i in self.detectors] + + def readable_monitor_count_uncertainties(self, dae: "SimpleDae") -> list[Device]: + """Publish individual uncertainty signals for all monitors.""" + + return [self.monitors[i].stddev for i in self.monitors] diff --git a/tests/devices/simpledae/test_reducers.py b/tests/devices/simpledae/test_reducers.py index a497f28..3b6ea88 100644 --- a/tests/devices/simpledae/test_reducers.py +++ b/tests/devices/simpledae/test_reducers.py @@ -1,3 +1,4 @@ +import math from unittest.mock import AsyncMock import pytest @@ -45,6 +46,9 @@ def __init__(self): self.period = FakePeriod() +# Scalar Normalizer + + async def test_period_good_frames_normalizer_publishes_period_good_frames( period_good_frames_reducer: PeriodGoodFramesNormalizer, ): @@ -67,6 +71,15 @@ async def test_good_frames_normalizer_publishes_good_frames( assert good_frames_reducer.denominator(fake_dae) == fake_dae.good_frames +async def test_scalar_normalizer_publishes_uncertainties( + simpledae: SimpleDae, + good_frames_reducer: GoodFramesNormalizer, +): + readables = good_frames_reducer.additional_readable_signals_uncertainties(simpledae) + assert good_frames_reducer.intensity_stddev in readables + assert good_frames_reducer.det_counts_stddev in readables + + async def test_period_good_frames_normalizer( simpledae: SimpleDae, period_good_frames_reducer: PeriodGoodFramesNormalizer, @@ -96,6 +109,46 @@ async def test_period_good_frames_normalizer( assert intensity == pytest.approx(170.731707317) +async def test_period_good_frames_normalizer_uncertainties( + simpledae: SimpleDae, + period_good_frames_reducer: PeriodGoodFramesNormalizer, +): + set_mock_value(simpledae.period.good_frames, 123) + + period_good_frames_reducer.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), + coords={"tof": sc.array(dims=["tof"], values=[0, 1, 2, 3])}, + ) + ) + period_good_frames_reducer.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), + coords={"tof": sc.array(dims=["tof"], values=[0, 1, 2, 3])}, + ) + ) + + await period_good_frames_reducer.reduce_data(simpledae) + + det_counts_stddev = await period_good_frames_reducer.det_counts_stddev.get_value() + intensity_stddev = await period_good_frames_reducer.intensity_stddev.get_value() + + assert det_counts_stddev == math.sqrt(21000) + assert intensity_stddev == pytest.approx(math.sqrt((21000 + (123**2 / 21000) ) / 123**2), 1e-4) + + +async def test_scalar_normalizer_publishes_individual_detector_count_uncertainties( + simpledae: SimpleDae, + period_good_frames_reducer: PeriodGoodFramesNormalizer, +): + + readables = period_good_frames_reducer.readable_detector_count_uncertainties(simpledae) + assert period_good_frames_reducer.detectors[1].stddev in readables + + +# Monitor Normalizer + + async def test_monitor_normalizer(simpledae: SimpleDae, monitor_normalizer: MonitorNormalizer): monitor_normalizer.detectors[1].read_spectrum_dataarray = AsyncMock( return_value=sc.DataArray( @@ -121,6 +174,31 @@ async def test_monitor_normalizer(simpledae: SimpleDae, monitor_normalizer: Moni assert intensity == pytest.approx(6000 / 15000) +async def test_monitor_normalizer_uncertainties(simpledae: SimpleDae, monitor_normalizer: MonitorNormalizer): + monitor_normalizer.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), + coords={"tof": sc.array(dims=["tof"], values=[0, 1, 2, 3])}, + ) + ) + monitor_normalizer.monitors[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), + coords={"tof": sc.array(dims=["tof"], values=[0, 1, 2, 3])}, + ) + ) + + await monitor_normalizer.reduce_data(simpledae) + + det_counts_stddev = await monitor_normalizer.det_counts_stddev.get_value() + mon_counts_stddev = await monitor_normalizer.mon_counts_stddev.get_value() + intensity_stddev = await monitor_normalizer.intensity_stddev.get_value() + + assert det_counts_stddev == math.sqrt(6000) + assert mon_counts_stddev == math.sqrt(15000) + assert intensity_stddev == pytest.approx(math.sqrt((6000 + (6000**2 / 15000) ) / 15000**2), 1e-4) + + async def test_monitor_normalizer_publishes_raw_and_normalized_counts( simpledae: SimpleDae, monitor_normalizer: MonitorNormalizer, @@ -129,3 +207,29 @@ async def test_monitor_normalizer_publishes_raw_and_normalized_counts( assert monitor_normalizer.intensity in readables assert monitor_normalizer.det_counts in readables assert monitor_normalizer.mon_counts in readables + + +async def test_monitor_normalizer_publishes_raw_and_normalized_count_uncertainties( + simpledae: SimpleDae, + monitor_normalizer: MonitorNormalizer, +): + readables = monitor_normalizer.additional_readable_signals_uncertainties(simpledae) + 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_monitor_normalizer_publishes_individual_detector_count_uncertainties( + simpledae: SimpleDae, + monitor_normalizer: MonitorNormalizer, +): + readables = monitor_normalizer.readable_detector_count_uncertainties(simpledae) + assert monitor_normalizer.detectors[1].stddev in readables + + +async def test_monitor_normalizer_publishes_individual_monitor_count_uncertainties( + simpledae: SimpleDae, + monitor_normalizer: MonitorNormalizer, +): + readables = monitor_normalizer.readable_monitor_count_uncertainties(simpledae) + assert monitor_normalizer.monitors[2].stddev in readables From 695ad61b3c90382527686493fce000051721924a Mon Sep 17 00:00:00 2001 From: Jack Doughty Date: Tue, 8 Oct 2024 15:32:30 +0100 Subject: [PATCH 02/17] Update docs --- .../002-use-ophyd-async.md | 2 ++ doc/architectural_decisions/004-use-scipp.md | 20 +++++++++++++++++++ doc/devices/dae.md | 7 +++++++ 3 files changed, 29 insertions(+) create mode 100644 doc/architectural_decisions/004-use-scipp.md diff --git a/doc/architectural_decisions/002-use-ophyd-async.md b/doc/architectural_decisions/002-use-ophyd-async.md index 944c3cd..7cfb0b4 100644 --- a/doc/architectural_decisions/002-use-ophyd-async.md +++ b/doc/architectural_decisions/002-use-ophyd-async.md @@ -35,3 +35,5 @@ We will use `ophyd-async`. - Developers will need to understand more details about `asyncio` - Developers will have to write somewhat less boilerplate code in `ophyd-async` as compared to `ophyd` - Our devices should be more comparable to other facilities on site (e.g. DLS) who are using `ophyd-async`. +- Less expertise with this library on site (mitigation: don't do too much which is very complicated with it) +- Potentially duplicates some of mantid's functionality: (mitigation: use scipp for "simple" things, use mantid in future if people want to do "full" data reduction pipelines) \ No newline at end of file diff --git a/doc/architectural_decisions/004-use-scipp.md b/doc/architectural_decisions/004-use-scipp.md new file mode 100644 index 0000000..38278cc --- /dev/null +++ b/doc/architectural_decisions/004-use-scipp.md @@ -0,0 +1,20 @@ +# Use `scipp` + +## Status + +Current + +## Context + +A decision needs to be made about whether to use scipp, numpy, uncertainties or develop our own library for the purpose of providing support for generating uncertanties on our counts data. + +# Decision + +We will be using scipp. + +# Justification & Consequences + +- `scipp` is being developed at ESS with past input from STFC, so is well suited for neutron counts data. +- `scipp` has a `numpy`-like interface but handles units and uncertainties by default under-the-hood. +- Neither `numpy` or `uncertanties` have exactly the functionality we would need, so the solution using them would be a mix of the libraries and our own code, there would be more places to go wrong. Maintainability. +- Developing our own uncertainties library will take time to understand and then implement. All of the functionality that we need has been done beforehand, so better to not waste time & effort. \ No newline at end of file diff --git a/doc/devices/dae.md b/doc/devices/dae.md index bff0aa9..9a660a2 100644 --- a/doc/devices/dae.md +++ b/doc/devices/dae.md @@ -177,6 +177,8 @@ Published signals: - `simpledae.good_frames` - the number of good frames reported by the DAE - `reducer.det_counts` - summed detector counts for all of the user-provided spectra - `reducer.intensity` - normalized intensity (`det_counts / good_frames`) +- `reducer.det_counts_stddev` - uncertainty (standard deviation) of the summed detector counts +- `reducer.intensity_stddev` - uncertainty (standard deviation) of the normalised intensity ### PeriodGoodFramesNormalizer @@ -187,6 +189,8 @@ Published signals: - `simpledae.period.good_frames` - the number of good frames reported by the DAE - `reducer.det_counts` - summed detector counts for all of the user-provided spectra - `reducer.intensity` - normalized intensity (`det_counts / good_frames`) +- `reducer.det_counts_stddev` - uncertainty (standard deviation) of the summed detector counts +- `reducer.intensity_stddev` - uncertainty (standard deviation) of the normalised intensity ### DetectorMonitorNormalizer @@ -197,6 +201,9 @@ Published signals: - `reducer.det_counts` - summed detector counts for the user-provided detector spectra - `reducer.mon_counts` - summed monitor counts for the user-provided monitor spectra - `reducer.intensity` - normalized intensity (`det_counts / mon_counts`) +- `reducer.det_counts_stddev` - uncertainty (standard deviation) of the summed detector counts +- `reducer.mon_counts_stddev` - uncertainty (standard deviation) of the summed monitor counts +- `reducer.intensity_stddev` - uncertainty (standard deviation) of the normalised intensity ## Waiters From ac720b9f905f067d18e36a70858c6e8d01471d65 Mon Sep 17 00:00:00 2001 From: Jack Doughty Date: Tue, 8 Oct 2024 15:34:42 +0100 Subject: [PATCH 03/17] Update docs --- doc/architectural_decisions/002-use-ophyd-async.md | 4 +--- doc/architectural_decisions/004-use-scipp.md | 4 +++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/architectural_decisions/002-use-ophyd-async.md b/doc/architectural_decisions/002-use-ophyd-async.md index 7cfb0b4..441f2c7 100644 --- a/doc/architectural_decisions/002-use-ophyd-async.md +++ b/doc/architectural_decisions/002-use-ophyd-async.md @@ -34,6 +34,4 @@ We will use `ophyd-async`. - `ophyd-async` will allow us to do fly scanning, if required in future, more easily than `ophyd` - Developers will need to understand more details about `asyncio` - Developers will have to write somewhat less boilerplate code in `ophyd-async` as compared to `ophyd` -- Our devices should be more comparable to other facilities on site (e.g. DLS) who are using `ophyd-async`. -- Less expertise with this library on site (mitigation: don't do too much which is very complicated with it) -- Potentially duplicates some of mantid's functionality: (mitigation: use scipp for "simple" things, use mantid in future if people want to do "full" data reduction pipelines) \ No newline at end of file +- Our devices should be more comparable to other facilities on site (e.g. DLS) who are using `ophyd-async`. \ No newline at end of file diff --git a/doc/architectural_decisions/004-use-scipp.md b/doc/architectural_decisions/004-use-scipp.md index 38278cc..2df0c49 100644 --- a/doc/architectural_decisions/004-use-scipp.md +++ b/doc/architectural_decisions/004-use-scipp.md @@ -17,4 +17,6 @@ We will be using scipp. - `scipp` is being developed at ESS with past input from STFC, so is well suited for neutron counts data. - `scipp` has a `numpy`-like interface but handles units and uncertainties by default under-the-hood. - Neither `numpy` or `uncertanties` have exactly the functionality we would need, so the solution using them would be a mix of the libraries and our own code, there would be more places to go wrong. Maintainability. -- Developing our own uncertainties library will take time to understand and then implement. All of the functionality that we need has been done beforehand, so better to not waste time & effort. \ No newline at end of file +- Developing our own uncertainties library will take time to understand and then implement. All of the functionality that we need has been done beforehand, so better to not waste time & effort. +- Less expertise with this library on site (mitigation: don't do too much which is very complicated with it) +- Potentially duplicates some of `mantid`'s functionality: (mitigation: use `scipp` for "simple" things, use `mantid` in future if people want to do "full" data reduction pipelines) \ No newline at end of file From 58b87d655158bd07867e67464097caeb8c43c8b3 Mon Sep 17 00:00:00 2001 From: Jack Doughty Date: Tue, 8 Oct 2024 15:41:56 +0100 Subject: [PATCH 04/17] Update docs --- doc/architectural_decisions/004-use-scipp.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/architectural_decisions/004-use-scipp.md b/doc/architectural_decisions/004-use-scipp.md index 2df0c49..efccade 100644 --- a/doc/architectural_decisions/004-use-scipp.md +++ b/doc/architectural_decisions/004-use-scipp.md @@ -17,6 +17,7 @@ We will be using scipp. - `scipp` is being developed at ESS with past input from STFC, so is well suited for neutron counts data. - `scipp` has a `numpy`-like interface but handles units and uncertainties by default under-the-hood. - Neither `numpy` or `uncertanties` have exactly the functionality we would need, so the solution using them would be a mix of the libraries and our own code, there would be more places to go wrong. Maintainability. +- `uncertainties` package tracks correlations so may have bad scaling on "large" arrays like counts data from the DAE. - Developing our own uncertainties library will take time to understand and then implement. All of the functionality that we need has been done beforehand, so better to not waste time & effort. - Less expertise with this library on site (mitigation: don't do too much which is very complicated with it) - Potentially duplicates some of `mantid`'s functionality: (mitigation: use `scipp` for "simple" things, use `mantid` in future if people want to do "full" data reduction pipelines) \ No newline at end of file From 0f5a152ce9cf007a90fe2a33967721e2bbdcf54c Mon Sep 17 00:00:00 2001 From: Jack Doughty Date: Wed, 9 Oct 2024 09:26:47 +0100 Subject: [PATCH 05/17] Refactoring --- src/ibex_bluesky_core/devices/dae/dae_spectra.py | 4 ++-- src/ibex_bluesky_core/devices/simpledae/reducers.py | 12 +----------- tests/devices/simpledae/test_reducers.py | 4 ++-- 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/src/ibex_bluesky_core/devices/dae/dae_spectra.py b/src/ibex_bluesky_core/devices/dae/dae_spectra.py index 3378fc3..5a042bb 100644 --- a/src/ibex_bluesky_core/devices/dae/dae_spectra.py +++ b/src/ibex_bluesky_core/devices/dae/dae_spectra.py @@ -56,7 +56,7 @@ def __init__(self, dae_prefix: str, *, spectra: int, period: int, name: str = "" int, f"{dae_prefix}SPEC:{period}:{spectra}:YC.NORD" ) - self.stddev, self._stddev_setter = soft_signal_r_and_setter(NDArray[float32], [0.0]) + self.stddev, self._stddev_setter = soft_signal_r_and_setter(NDArray[float32], []) super().__init__(name=name) @@ -85,7 +85,7 @@ async def read_counts_per_time(self) -> NDArray[float32]: async def read_counts_uncertainties(self) -> NDArray[float32]: """Read a correctly-sized array of uncertainties for each count.""" - return await self._read_sized(self.stddev, self.counts_size) # type: ignore + return await self._read_sized(self.stddev, self.counts_size) # type: ignore ??????? async def read_spectrum_dataarray(self) -> sc.DataArray: """Get a scipp DataArray containing the current data from this spectrum. diff --git a/src/ibex_bluesky_core/devices/simpledae/reducers.py b/src/ibex_bluesky_core/devices/simpledae/reducers.py index 77568fe..587db01 100644 --- a/src/ibex_bluesky_core/devices/simpledae/reducers.py +++ b/src/ibex_bluesky_core/devices/simpledae/reducers.py @@ -2,7 +2,7 @@ import asyncio from abc import ABCMeta, abstractmethod -from typing import TYPE_CHECKING, Collection, Sequence, Tuple +from typing import TYPE_CHECKING, Collection, Sequence import numpy as np import scipp as sc @@ -91,11 +91,6 @@ def additional_readable_signals(self, dae: "SimpleDae") -> list[Device]: self.det_counts, self.intensity, self.denominator(dae), - ] - - def additional_readable_signals_uncertainties(self, dae: "SimpleDae") -> list[Device]: - """Publish interesting signals derived or used by this reducer, including respective uncertainties.""" - return [ self.det_counts_stddev, self.intensity_stddev, ] @@ -186,11 +181,6 @@ def additional_readable_signals(self, dae: "SimpleDae") -> list[Device]: self.det_counts, self.mon_counts, self.intensity, - ] - - def additional_readable_signals_uncertainties(self, dae: "SimpleDae") -> list[Device]: - """Publish interesting signals derived or used by this reducer, including respective uncertainties.""" - return [ self.det_counts_stddev, self.mon_counts_stddev, self.intensity_stddev, diff --git a/tests/devices/simpledae/test_reducers.py b/tests/devices/simpledae/test_reducers.py index 3b6ea88..5f046cd 100644 --- a/tests/devices/simpledae/test_reducers.py +++ b/tests/devices/simpledae/test_reducers.py @@ -75,7 +75,7 @@ async def test_scalar_normalizer_publishes_uncertainties( simpledae: SimpleDae, good_frames_reducer: GoodFramesNormalizer, ): - readables = good_frames_reducer.additional_readable_signals_uncertainties(simpledae) + readables = good_frames_reducer.additional_readable_signals(simpledae) assert good_frames_reducer.intensity_stddev in readables assert good_frames_reducer.det_counts_stddev in readables @@ -213,7 +213,7 @@ async def test_monitor_normalizer_publishes_raw_and_normalized_count_uncertainti simpledae: SimpleDae, monitor_normalizer: MonitorNormalizer, ): - readables = monitor_normalizer.additional_readable_signals_uncertainties(simpledae) + readables = monitor_normalizer.additional_readable_signals(simpledae) assert monitor_normalizer.intensity_stddev in readables assert monitor_normalizer.det_counts_stddev in readables assert monitor_normalizer.mon_counts_stddev in readables From e38024a441ba9b3c05e9d0035f3deb066d9d2af8 Mon Sep 17 00:00:00 2001 From: Jack Doughty Date: Wed, 9 Oct 2024 16:29:20 +0100 Subject: [PATCH 06/17] Refactoring --- manual_system_tests/dae_scan.py | 2 + .../devices/dae/dae_spectra.py | 77 +++++++++++++++---- .../devices/simpledae/reducers.py | 37 +++------ tests/devices/simpledae/test_reducers.py | 25 ------ tests/devices/test_dae.py | 10 ++- 5 files changed, 80 insertions(+), 71 deletions(-) diff --git a/manual_system_tests/dae_scan.py b/manual_system_tests/dae_scan.py index d26cc6c..adcd00e 100644 --- a/manual_system_tests/dae_scan.py +++ b/manual_system_tests/dae_scan.py @@ -75,7 +75,9 @@ def dae_scan_plan() -> Generator[Msg, None, None]: block.name, controller.run_number.name, reducer.intensity.name, + reducer.intensity_stddev.name, reducer.det_counts.name, + reducer.det_counts_stddev.name, dae.good_frames.name, ] ), diff --git a/src/ibex_bluesky_core/devices/dae/dae_spectra.py b/src/ibex_bluesky_core/devices/dae/dae_spectra.py index 5a042bb..39cded9 100644 --- a/src/ibex_bluesky_core/devices/dae/dae_spectra.py +++ b/src/ibex_bluesky_core/devices/dae/dae_spectra.py @@ -1,23 +1,49 @@ """ophyd-async devices and utilities for a single DAE spectra.""" import asyncio - +from bluesky.protocols import Triggerable import scipp as sc +import typing +import numpy as np from event_model.documents.event_descriptor import DataKey from numpy import float32 from numpy.typing import NDArray -from ophyd_async.core import SignalR, StandardReadable, soft_signal_r_and_setter +from ophyd_async.core import SignalR, StandardReadable, soft_signal_r_and_setter, AsyncStageable, AsyncStatus from ophyd_async.epics.signal import epics_signal_r -class DaeSpectra(StandardReadable): +# def soft_signal_r_and_setter( +# datatype: type[T] | None = None, +# initial_value: T | None = None, +# name: str = "", +# units: str | None = None, +# precision: int | None = None, +# ) -> tuple[SignalR[T], Callable[[T], None]]: +# """Returns a tuple of a read-only Signal and a callable through +# which the signal can be internally modified within the device. +# May pass metadata, which are propagated into describe. +# Use soft_signal_rw if you want a device that is externally modifiable +# """ +# metadata = SignalMetadata(units=units, precision=precision) +# backend = SoftSignalBackend(datatype, initial_value, metadata=metadata) +# signal = SignalR(backend, name=name) + +# return (signal, backend.set_value, lambda u: metadata.units = u) + + +# def get_units(sig: SignalR[Any]) -> str: +# pass + + +class DaeSpectra(StandardReadable, Triggerable): """Subdevice for a single DAE spectra.""" + def __init__(self, dae_prefix: str, *, spectra: int, period: int, name: str = "") -> None: """Set up signals for a single DAE spectra.""" # x-axis; time-of-flight. # These are bin-centre coordinates. - self.tof: SignalR[NDArray[float32]] = epics_signal_r( + self._tof_raw: SignalR[NDArray[float32]] = epics_signal_r( NDArray[float32], f"{dae_prefix}SPEC:{period}:{spectra}:X" ) self.tof_size: SignalR[int] = epics_signal_r( @@ -26,7 +52,7 @@ def __init__(self, dae_prefix: str, *, spectra: int, period: int, name: str = "" # x-axis; time-of-flight. # These are bin-edge coordinates, with a size one more than the corresponding data. - self.tof_edges: SignalR[NDArray[float32]] = epics_signal_r( + self._tof_edges_raw: SignalR[NDArray[float32]] = epics_signal_r( NDArray[float32], f"{dae_prefix}SPEC:{period}:{spectra}:XE" ) self.tof_edges_size: SignalR[int] = epics_signal_r( @@ -38,7 +64,7 @@ def __init__(self, dae_prefix: str, *, spectra: int, period: int, name: str = "" # that ToF bin. # - Unsuitable for summing counts directly. # - Will give a continuous plot for non-uniform bin sizes. - self.counts_per_time: SignalR[NDArray[float32]] = epics_signal_r( + self._counts_per_time_raw: SignalR[NDArray[float32]] = epics_signal_r( NDArray[float32], f"{dae_prefix}SPEC:{period}:{spectra}:Y" ) self.counts_per_time_size: SignalR[int] = epics_signal_r( @@ -49,17 +75,34 @@ def __init__(self, dae_prefix: str, *, spectra: int, period: int, name: str = "" # This is unnormalized number of counts per ToF bin. # - Suitable for summing counts # - This will give a discontinuous plot for non-uniform bin sizes. - self.counts: SignalR[NDArray[float32]] = epics_signal_r( + self._counts_raw: SignalR[NDArray[float32]] = epics_signal_r( NDArray[float32], f"{dae_prefix}SPEC:{period}:{spectra}:YC" ) self.counts_size: SignalR[int] = epics_signal_r( int, f"{dae_prefix}SPEC:{period}:{spectra}:YC.NORD" ) + self.tof, self._tof_setter = soft_signal_r_and_setter(NDArray[float32], []) + self.tof_edges, self._tof_edges_setter = soft_signal_r_and_setter(NDArray[float32], []) + self.counts_per_time, self._counts_per_time_setter = soft_signal_r_and_setter(NDArray[float32], []) + self.counts, self._counts_setter = soft_signal_r_and_setter(NDArray[float32], []) self.stddev, self._stddev_setter = soft_signal_r_and_setter(NDArray[float32], []) super().__init__(name=name) + + @AsyncStatus.wrap + async def trigger(self) -> None: + + self._tof_setter(await self._read_sized(self._tof_raw, self.tof_size)) + self._tof_edges_setter(await self._read_sized(self._tof_edges_raw, self.tof_edges_size)) + self._counts_per_time_setter(await self._read_sized(self._counts_per_time_raw, self.counts_per_time_size)) + self._counts_setter(await self._read_sized(self._counts_raw, self.counts_size)) + + stddev = await self.counts.get_value() + self._stddev_setter(np.sqrt(stddev)) + + async def _read_sized( self, array_signal: SignalR[NDArray[float32]], size_signal: SignalR[int] ) -> NDArray[float32]: @@ -68,24 +111,28 @@ async def _read_sized( async def read_tof(self) -> NDArray[float32]: """Read a correctly-sized time-of-flight (x) array representing bin centres.""" - return await self._read_sized(self.tof, self.tof_size) + tof = await self.tof.get_value() + return typing.cast(NDArray[float32], tof) async def read_tof_edges(self) -> NDArray[float32]: """Read a correctly-sized time-of-flight (x) array representing bin edges.""" - return await self._read_sized(self.tof_edges, self.tof_edges_size) + tof_edges = await self.tof_edges.get_value() + return typing.cast(NDArray[float32], tof_edges) async def read_counts(self) -> NDArray[float32]: """Read a correctly-sized array of counts.""" - return await self._read_sized(self.counts, self.counts_size) + counts = await self.counts.get_value() + return typing.cast(NDArray[float32], counts) async def read_counts_per_time(self) -> NDArray[float32]: """Read a correctly-sized array of counts divided by bin width.""" - return await self._read_sized(self.counts_per_time, self.counts_per_time_size) + counts_per_time = await self.counts_per_time.get_value() + return typing.cast(NDArray[float32], counts_per_time) async def read_counts_uncertainties(self) -> NDArray[float32]: """Read a correctly-sized array of uncertainties for each count.""" - - return await self._read_sized(self.stddev, self.counts_size) # type: ignore ??????? + stddev = await self.stddev.get_value() + return typing.cast(NDArray[float32], stddev) async def read_spectrum_dataarray(self) -> sc.DataArray: """Get a scipp DataArray containing the current data from this spectrum. @@ -98,7 +145,7 @@ async def read_spectrum_dataarray(self) -> sc.DataArray: """ tof_edges, tof_edges_descriptor, counts = await asyncio.gather( self.read_tof_edges(), - self.tof_edges.describe(), + self._tof_edges_raw.describe(), self.read_counts(), ) @@ -109,7 +156,7 @@ async def read_spectrum_dataarray(self) -> sc.DataArray: f"Edges size was {tof_edges.size}, counts size was {counts.size}." ) - datakey: DataKey = tof_edges_descriptor[self.tof_edges.name] + datakey: DataKey = tof_edges_descriptor[self._tof_edges_raw.name] unit = datakey.get("units", None) if unit is None: raise ValueError("Could not determine engineering units of tof edges.") diff --git a/src/ibex_bluesky_core/devices/simpledae/reducers.py b/src/ibex_bluesky_core/devices/simpledae/reducers.py index 587db01..50e0b0c 100644 --- a/src/ibex_bluesky_core/devices/simpledae/reducers.py +++ b/src/ibex_bluesky_core/devices/simpledae/reducers.py @@ -66,6 +66,9 @@ def denominator(self, dae: "SimpleDae") -> SignalR[int] | SignalR[float]: async def reduce_data(self, dae: "SimpleDae") -> None: """Apply the normalization.""" + + await asyncio.gather(*[self.detectors[i].trigger() for i in self.detectors]) + summed_counts, denominator = await asyncio.gather( sum_spectra(self.detectors.values()), self.denominator(dae).get_value() ) @@ -81,10 +84,6 @@ async def reduce_data(self, dae: "SimpleDae") -> None: self._det_counts_stddev_setter(math.sqrt(detector_counts_var)) self._intensity_stddev_setter(math.sqrt(intensity_var)) - for spec in self.detectors.values(): - stddev = await spec.read_counts() - spec._stddev_setter(np.sqrt(stddev)) - def additional_readable_signals(self, dae: "SimpleDae") -> list[Device]: """Publish interesting signals derived or used by this reducer.""" return [ @@ -94,11 +93,6 @@ def additional_readable_signals(self, dae: "SimpleDae") -> list[Device]: self.det_counts_stddev, self.intensity_stddev, ] - - def readable_detector_count_uncertainties(self, dae: "SimpleDae") -> list[Device]: - """Publish individual uncertainty signals for all detectors.""" - - return [self.detectors[i].stddev for i in self.detectors] class PeriodGoodFramesNormalizer(ScalarNormalizer): @@ -151,6 +145,13 @@ def __init__( async def reduce_data(self, dae: "SimpleDae") -> None: """Apply the normalization.""" + async def trigger_detectors(): + await asyncio.gather(*[self.detectors[i].trigger() for i in self.detectors]) + async def trigger_monitors(): + await asyncio.gather(*[self.monitors[i].trigger() for i in self.monitors]) + + await asyncio.gather(trigger_detectors(), trigger_monitors()) + detector_counts, monitor_counts = await asyncio.gather( sum_spectra(self.detectors.values()), sum_spectra(self.monitors.values()) ) @@ -167,14 +168,6 @@ async def reduce_data(self, dae: "SimpleDae") -> None: self._mon_counts_stddev_setter(math.sqrt(monitor_counts_var)) self._intensity_stddev_setter(math.sqrt(intensity_var)) - for spec in self.detectors.values(): - stddev = await spec.read_counts() - spec._stddev_setter(np.sqrt(stddev)) - - for spec in self.monitors.values(): - stddev = await spec.read_counts() - spec._stddev_setter(np.sqrt(stddev)) - def additional_readable_signals(self, dae: "SimpleDae") -> list[Device]: """Publish interesting signals derived or used by this reducer.""" return [ @@ -185,13 +178,3 @@ def additional_readable_signals(self, dae: "SimpleDae") -> list[Device]: self.mon_counts_stddev, self.intensity_stddev, ] - - def readable_detector_count_uncertainties(self, dae: "SimpleDae") -> list[Device]: - """Publish individual uncertainty signals for all detectors.""" - - return [self.detectors[i].stddev for i in self.detectors] - - def readable_monitor_count_uncertainties(self, dae: "SimpleDae") -> list[Device]: - """Publish individual uncertainty signals for all monitors.""" - - return [self.monitors[i].stddev for i in self.monitors] diff --git a/tests/devices/simpledae/test_reducers.py b/tests/devices/simpledae/test_reducers.py index 5f046cd..1a5f3df 100644 --- a/tests/devices/simpledae/test_reducers.py +++ b/tests/devices/simpledae/test_reducers.py @@ -137,15 +137,6 @@ async def test_period_good_frames_normalizer_uncertainties( assert intensity_stddev == pytest.approx(math.sqrt((21000 + (123**2 / 21000) ) / 123**2), 1e-4) -async def test_scalar_normalizer_publishes_individual_detector_count_uncertainties( - simpledae: SimpleDae, - period_good_frames_reducer: PeriodGoodFramesNormalizer, -): - - readables = period_good_frames_reducer.readable_detector_count_uncertainties(simpledae) - assert period_good_frames_reducer.detectors[1].stddev in readables - - # Monitor Normalizer @@ -217,19 +208,3 @@ 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_monitor_normalizer_publishes_individual_detector_count_uncertainties( - simpledae: SimpleDae, - monitor_normalizer: MonitorNormalizer, -): - readables = monitor_normalizer.readable_detector_count_uncertainties(simpledae) - assert monitor_normalizer.detectors[1].stddev in readables - - -async def test_monitor_normalizer_publishes_individual_monitor_count_uncertainties( - simpledae: SimpleDae, - monitor_normalizer: MonitorNormalizer, -): - readables = monitor_normalizer.readable_monitor_count_uncertainties(simpledae) - assert monitor_normalizer.monitors[2].stddev in readables diff --git a/tests/devices/test_dae.py b/tests/devices/test_dae.py index 181de3a..0bc124c 100644 --- a/tests/devices/test_dae.py +++ b/tests/devices/test_dae.py @@ -950,15 +950,17 @@ def test_empty_dae_settings_dataclass_does_not_change_any_settings(dae: Dae, RE: async def test_read_spectra_correctly_sizes_arrays(spectrum: DaeSpectra): - set_mock_value(spectrum.tof, np.zeros(dtype=np.float32, shape=(1000,))) + set_mock_value(spectrum._tof_raw, np.zeros(dtype=np.float32, shape=(1000,))) set_mock_value(spectrum.tof_size, 100) - set_mock_value(spectrum.counts, np.zeros(dtype=np.float32, shape=(2000,))) + set_mock_value(spectrum._counts_raw, np.zeros(dtype=np.float32, shape=(2000,))) set_mock_value(spectrum.counts_size, 200) - set_mock_value(spectrum.counts_per_time, np.zeros(dtype=np.float32, shape=(3000,))) + set_mock_value(spectrum._counts_per_time_raw, np.zeros(dtype=np.float32, shape=(3000,))) set_mock_value(spectrum.counts_per_time_size, 300) - set_mock_value(spectrum.tof_edges, np.zeros(dtype=np.float32, shape=(4000,))) + set_mock_value(spectrum._tof_edges_raw, np.zeros(dtype=np.float32, shape=(4000,))) set_mock_value(spectrum.tof_edges_size, 400) + await spectrum.trigger() + assert (await spectrum.read_tof()).shape == (100,) assert (await spectrum.read_counts()).shape == (200,) assert (await spectrum.read_counts_per_time()).shape == (300,) From f0af97340569dab7d950a0bb6672d2a6e71d0ec8 Mon Sep 17 00:00:00 2001 From: Jack Doughty Date: Thu, 10 Oct 2024 13:02:15 +0100 Subject: [PATCH 07/17] Removed support for individual spectra uncertainties --- .../devices/dae/dae_spectra.py | 80 +++---------------- .../devices/simpledae/reducers.py | 8 -- tests/devices/test_dae.py | 10 +-- 3 files changed, 17 insertions(+), 81 deletions(-) diff --git a/src/ibex_bluesky_core/devices/dae/dae_spectra.py b/src/ibex_bluesky_core/devices/dae/dae_spectra.py index 39cded9..0c90469 100644 --- a/src/ibex_bluesky_core/devices/dae/dae_spectra.py +++ b/src/ibex_bluesky_core/devices/dae/dae_spectra.py @@ -1,49 +1,23 @@ """ophyd-async devices and utilities for a single DAE spectra.""" import asyncio -from bluesky.protocols import Triggerable + import scipp as sc -import typing -import numpy as np from event_model.documents.event_descriptor import DataKey from numpy import float32 from numpy.typing import NDArray -from ophyd_async.core import SignalR, StandardReadable, soft_signal_r_and_setter, AsyncStageable, AsyncStatus +from ophyd_async.core import SignalR, StandardReadable from ophyd_async.epics.signal import epics_signal_r -# def soft_signal_r_and_setter( -# datatype: type[T] | None = None, -# initial_value: T | None = None, -# name: str = "", -# units: str | None = None, -# precision: int | None = None, -# ) -> tuple[SignalR[T], Callable[[T], None]]: -# """Returns a tuple of a read-only Signal and a callable through -# which the signal can be internally modified within the device. -# May pass metadata, which are propagated into describe. -# Use soft_signal_rw if you want a device that is externally modifiable -# """ -# metadata = SignalMetadata(units=units, precision=precision) -# backend = SoftSignalBackend(datatype, initial_value, metadata=metadata) -# signal = SignalR(backend, name=name) - -# return (signal, backend.set_value, lambda u: metadata.units = u) - - -# def get_units(sig: SignalR[Any]) -> str: -# pass - - -class DaeSpectra(StandardReadable, Triggerable): +class DaeSpectra(StandardReadable): """Subdevice for a single DAE spectra.""" - def __init__(self, dae_prefix: str, *, spectra: int, period: int, name: str = "") -> None: """Set up signals for a single DAE spectra.""" # x-axis; time-of-flight. # These are bin-centre coordinates. - self._tof_raw: SignalR[NDArray[float32]] = epics_signal_r( + self.tof: SignalR[NDArray[float32]] = epics_signal_r( NDArray[float32], f"{dae_prefix}SPEC:{period}:{spectra}:X" ) self.tof_size: SignalR[int] = epics_signal_r( @@ -52,7 +26,7 @@ def __init__(self, dae_prefix: str, *, spectra: int, period: int, name: str = "" # x-axis; time-of-flight. # These are bin-edge coordinates, with a size one more than the corresponding data. - self._tof_edges_raw: SignalR[NDArray[float32]] = epics_signal_r( + self.tof_edges: SignalR[NDArray[float32]] = epics_signal_r( NDArray[float32], f"{dae_prefix}SPEC:{period}:{spectra}:XE" ) self.tof_edges_size: SignalR[int] = epics_signal_r( @@ -64,7 +38,7 @@ def __init__(self, dae_prefix: str, *, spectra: int, period: int, name: str = "" # that ToF bin. # - Unsuitable for summing counts directly. # - Will give a continuous plot for non-uniform bin sizes. - self._counts_per_time_raw: SignalR[NDArray[float32]] = epics_signal_r( + self.counts_per_time: SignalR[NDArray[float32]] = epics_signal_r( NDArray[float32], f"{dae_prefix}SPEC:{period}:{spectra}:Y" ) self.counts_per_time_size: SignalR[int] = epics_signal_r( @@ -75,34 +49,15 @@ def __init__(self, dae_prefix: str, *, spectra: int, period: int, name: str = "" # This is unnormalized number of counts per ToF bin. # - Suitable for summing counts # - This will give a discontinuous plot for non-uniform bin sizes. - self._counts_raw: SignalR[NDArray[float32]] = epics_signal_r( + self.counts: SignalR[NDArray[float32]] = epics_signal_r( NDArray[float32], f"{dae_prefix}SPEC:{period}:{spectra}:YC" ) self.counts_size: SignalR[int] = epics_signal_r( int, f"{dae_prefix}SPEC:{period}:{spectra}:YC.NORD" ) - self.tof, self._tof_setter = soft_signal_r_and_setter(NDArray[float32], []) - self.tof_edges, self._tof_edges_setter = soft_signal_r_and_setter(NDArray[float32], []) - self.counts_per_time, self._counts_per_time_setter = soft_signal_r_and_setter(NDArray[float32], []) - self.counts, self._counts_setter = soft_signal_r_and_setter(NDArray[float32], []) - self.stddev, self._stddev_setter = soft_signal_r_and_setter(NDArray[float32], []) - super().__init__(name=name) - - @AsyncStatus.wrap - async def trigger(self) -> None: - - self._tof_setter(await self._read_sized(self._tof_raw, self.tof_size)) - self._tof_edges_setter(await self._read_sized(self._tof_edges_raw, self.tof_edges_size)) - self._counts_per_time_setter(await self._read_sized(self._counts_per_time_raw, self.counts_per_time_size)) - self._counts_setter(await self._read_sized(self._counts_raw, self.counts_size)) - - stddev = await self.counts.get_value() - self._stddev_setter(np.sqrt(stddev)) - - async def _read_sized( self, array_signal: SignalR[NDArray[float32]], size_signal: SignalR[int] ) -> NDArray[float32]: @@ -111,28 +66,19 @@ async def _read_sized( async def read_tof(self) -> NDArray[float32]: """Read a correctly-sized time-of-flight (x) array representing bin centres.""" - tof = await self.tof.get_value() - return typing.cast(NDArray[float32], tof) + return await self._read_sized(self.tof, self.tof_size) async def read_tof_edges(self) -> NDArray[float32]: """Read a correctly-sized time-of-flight (x) array representing bin edges.""" - tof_edges = await self.tof_edges.get_value() - return typing.cast(NDArray[float32], tof_edges) + return await self._read_sized(self.tof_edges, self.tof_edges_size) async def read_counts(self) -> NDArray[float32]: """Read a correctly-sized array of counts.""" - counts = await self.counts.get_value() - return typing.cast(NDArray[float32], counts) + return await self._read_sized(self.counts, self.counts_size) async def read_counts_per_time(self) -> NDArray[float32]: """Read a correctly-sized array of counts divided by bin width.""" - counts_per_time = await self.counts_per_time.get_value() - return typing.cast(NDArray[float32], counts_per_time) - - async def read_counts_uncertainties(self) -> NDArray[float32]: - """Read a correctly-sized array of uncertainties for each count.""" - stddev = await self.stddev.get_value() - return typing.cast(NDArray[float32], stddev) + return await self._read_sized(self.counts_per_time, self.counts_per_time_size) async def read_spectrum_dataarray(self) -> sc.DataArray: """Get a scipp DataArray containing the current data from this spectrum. @@ -145,7 +91,7 @@ async def read_spectrum_dataarray(self) -> sc.DataArray: """ tof_edges, tof_edges_descriptor, counts = await asyncio.gather( self.read_tof_edges(), - self._tof_edges_raw.describe(), + self.tof_edges.describe(), self.read_counts(), ) @@ -156,7 +102,7 @@ async def read_spectrum_dataarray(self) -> sc.DataArray: f"Edges size was {tof_edges.size}, counts size was {counts.size}." ) - datakey: DataKey = tof_edges_descriptor[self._tof_edges_raw.name] + datakey: DataKey = tof_edges_descriptor[self.tof_edges.name] unit = datakey.get("units", None) if unit is None: raise ValueError("Could not determine engineering units of tof edges.") diff --git a/src/ibex_bluesky_core/devices/simpledae/reducers.py b/src/ibex_bluesky_core/devices/simpledae/reducers.py index 50e0b0c..4bf6208 100644 --- a/src/ibex_bluesky_core/devices/simpledae/reducers.py +++ b/src/ibex_bluesky_core/devices/simpledae/reducers.py @@ -67,8 +67,6 @@ def denominator(self, dae: "SimpleDae") -> SignalR[int] | SignalR[float]: async def reduce_data(self, dae: "SimpleDae") -> None: """Apply the normalization.""" - await asyncio.gather(*[self.detectors[i].trigger() for i in self.detectors]) - summed_counts, denominator = await asyncio.gather( sum_spectra(self.detectors.values()), self.denominator(dae).get_value() ) @@ -145,12 +143,6 @@ def __init__( async def reduce_data(self, dae: "SimpleDae") -> None: """Apply the normalization.""" - async def trigger_detectors(): - await asyncio.gather(*[self.detectors[i].trigger() for i in self.detectors]) - async def trigger_monitors(): - await asyncio.gather(*[self.monitors[i].trigger() for i in self.monitors]) - - await asyncio.gather(trigger_detectors(), trigger_monitors()) detector_counts, monitor_counts = await asyncio.gather( sum_spectra(self.detectors.values()), sum_spectra(self.monitors.values()) diff --git a/tests/devices/test_dae.py b/tests/devices/test_dae.py index 0bc124c..181de3a 100644 --- a/tests/devices/test_dae.py +++ b/tests/devices/test_dae.py @@ -950,17 +950,15 @@ def test_empty_dae_settings_dataclass_does_not_change_any_settings(dae: Dae, RE: async def test_read_spectra_correctly_sizes_arrays(spectrum: DaeSpectra): - set_mock_value(spectrum._tof_raw, np.zeros(dtype=np.float32, shape=(1000,))) + set_mock_value(spectrum.tof, np.zeros(dtype=np.float32, shape=(1000,))) set_mock_value(spectrum.tof_size, 100) - set_mock_value(spectrum._counts_raw, np.zeros(dtype=np.float32, shape=(2000,))) + set_mock_value(spectrum.counts, np.zeros(dtype=np.float32, shape=(2000,))) set_mock_value(spectrum.counts_size, 200) - set_mock_value(spectrum._counts_per_time_raw, np.zeros(dtype=np.float32, shape=(3000,))) + set_mock_value(spectrum.counts_per_time, np.zeros(dtype=np.float32, shape=(3000,))) set_mock_value(spectrum.counts_per_time_size, 300) - set_mock_value(spectrum._tof_edges_raw, np.zeros(dtype=np.float32, shape=(4000,))) + set_mock_value(spectrum.tof_edges, np.zeros(dtype=np.float32, shape=(4000,))) set_mock_value(spectrum.tof_edges_size, 400) - await spectrum.trigger() - assert (await spectrum.read_tof()).shape == (100,) assert (await spectrum.read_counts()).shape == (200,) assert (await spectrum.read_counts_per_time()).shape == (300,) From f7410ba94c24dee9e75b5bca9913c8e1bc9f8548 Mon Sep 17 00:00:00 2001 From: Jack Doughty Date: Thu, 10 Oct 2024 13:29:34 +0100 Subject: [PATCH 08/17] Ruff --- .../devices/simpledae/reducers.py | 27 +++++++++----- tests/devices/simpledae/test_reducers.py | 36 +++++++++++++++---- 2 files changed, 48 insertions(+), 15 deletions(-) diff --git a/src/ibex_bluesky_core/devices/simpledae/reducers.py b/src/ibex_bluesky_core/devices/simpledae/reducers.py index 4bf6208..b47004c 100644 --- a/src/ibex_bluesky_core/devices/simpledae/reducers.py +++ b/src/ibex_bluesky_core/devices/simpledae/reducers.py @@ -1,12 +1,11 @@ """DAE data reduction strategies.""" import asyncio +import math from abc import ABCMeta, abstractmethod from typing import TYPE_CHECKING, Collection, Sequence -import numpy as np import scipp as sc -import math from ophyd_async.core import ( Device, DeviceVector, @@ -55,8 +54,12 @@ def __init__(self, prefix: str, detector_spectra: Sequence[int]) -> None: self.det_counts, self._det_counts_setter = soft_signal_r_and_setter(float, 0.0) self.intensity, self._intensity_setter = soft_signal_r_and_setter(float, 0.0, precision=6) - self.det_counts_stddev, self._det_counts_stddev_setter = soft_signal_r_and_setter(float, 0.0) - self.intensity_stddev, self._intensity_stddev_setter = soft_signal_r_and_setter(float, 0.0, precision=6) + self.det_counts_stddev, self._det_counts_stddev_setter = soft_signal_r_and_setter( + float, 0.0 + ) + self.intensity_stddev, self._intensity_stddev_setter = soft_signal_r_and_setter( + float, 0.0, precision=6 + ) super().__init__(name="") @@ -135,9 +138,15 @@ def __init__( self.mon_counts, self._mon_counts_setter = soft_signal_r_and_setter(float, 0.0) self.intensity, self._intensity_setter = soft_signal_r_and_setter(float, 0.0, precision=6) - self.det_counts_stddev, self._det_counts_stddev_setter = soft_signal_r_and_setter(float, 0.0) - self.mon_counts_stddev, self._mon_counts_stddev_setter = soft_signal_r_and_setter(float, 0.0) - self.intensity_stddev, self._intensity_stddev_setter = soft_signal_r_and_setter(float, 0.0, precision=6) + self.det_counts_stddev, self._det_counts_stddev_setter = soft_signal_r_and_setter( + float, 0.0 + ) + self.mon_counts_stddev, self._mon_counts_stddev_setter = soft_signal_r_and_setter( + float, 0.0 + ) + self.intensity_stddev, self._intensity_stddev_setter = soft_signal_r_and_setter( + float, 0.0, precision=6 + ) super().__init__(name="") @@ -154,7 +163,9 @@ async def reduce_data(self, dae: "SimpleDae") -> None: detector_counts_var = 0.0 if detector_counts.variance is None else detector_counts.variance monitor_counts_var = 0.0 if monitor_counts.variance is None else monitor_counts.variance - intensity_var = 0.0 if monitor_counts_var == 0.0 else (detector_counts / monitor_counts).variance + intensity_var = ( + 0.0 if monitor_counts_var == 0.0 else (detector_counts / monitor_counts).variance + ) self._det_counts_stddev_setter(math.sqrt(detector_counts_var)) self._mon_counts_stddev_setter(math.sqrt(monitor_counts_var)) diff --git a/tests/devices/simpledae/test_reducers.py b/tests/devices/simpledae/test_reducers.py index 1a5f3df..85f9e84 100644 --- a/tests/devices/simpledae/test_reducers.py +++ b/tests/devices/simpledae/test_reducers.py @@ -117,13 +117,23 @@ async def test_period_good_frames_normalizer_uncertainties( period_good_frames_reducer.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), + data=sc.Variable( + dims=["tof"], + values=[1000.0, 2000.0, 3000.0], + variances=[1000.0, 2000.0, 3000.0], + unit=sc.units.counts, + ), coords={"tof": sc.array(dims=["tof"], values=[0, 1, 2, 3])}, ) ) period_good_frames_reducer.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), + data=sc.Variable( + dims=["tof"], + values=[4000.0, 5000.0, 6000.0], + variances=[4000.0, 5000.0, 6000.0], + unit=sc.units.counts, + ), coords={"tof": sc.array(dims=["tof"], values=[0, 1, 2, 3])}, ) ) @@ -134,7 +144,7 @@ async def test_period_good_frames_normalizer_uncertainties( intensity_stddev = await period_good_frames_reducer.intensity_stddev.get_value() assert det_counts_stddev == math.sqrt(21000) - assert intensity_stddev == pytest.approx(math.sqrt((21000 + (123**2 / 21000) ) / 123**2), 1e-4) + assert intensity_stddev == pytest.approx(math.sqrt((21000 + (123**2 / 21000)) / 123**2), 1e-4) # Monitor Normalizer @@ -165,16 +175,28 @@ async def test_monitor_normalizer(simpledae: SimpleDae, monitor_normalizer: Moni assert intensity == pytest.approx(6000 / 15000) -async def test_monitor_normalizer_uncertainties(simpledae: SimpleDae, monitor_normalizer: MonitorNormalizer): +async def test_monitor_normalizer_uncertainties( + simpledae: SimpleDae, monitor_normalizer: MonitorNormalizer +): monitor_normalizer.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), + data=sc.Variable( + dims=["tof"], + values=[1000.0, 2000.0, 3000.0], + variances=[1000.0, 2000.0, 3000.0], + unit=sc.units.counts, + ), coords={"tof": sc.array(dims=["tof"], values=[0, 1, 2, 3])}, ) ) monitor_normalizer.monitors[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), + data=sc.Variable( + dims=["tof"], + values=[4000.0, 5000.0, 6000.0], + variances=[4000.0, 5000.0, 6000.0], + unit=sc.units.counts, + ), coords={"tof": sc.array(dims=["tof"], values=[0, 1, 2, 3])}, ) ) @@ -187,7 +209,7 @@ async def test_monitor_normalizer_uncertainties(simpledae: SimpleDae, monitor_no assert det_counts_stddev == math.sqrt(6000) assert mon_counts_stddev == math.sqrt(15000) - assert intensity_stddev == pytest.approx(math.sqrt((6000 + (6000**2 / 15000) ) / 15000**2), 1e-4) + assert intensity_stddev == pytest.approx(math.sqrt((6000 + (6000**2 / 15000)) / 15000**2), 1e-4) async def test_monitor_normalizer_publishes_raw_and_normalized_counts( From 3250cb3a55ccafcc24a33e2e95c661a434dfcfc5 Mon Sep 17 00:00:00 2001 From: Jack Doughty Date: Thu, 10 Oct 2024 13:31:16 +0100 Subject: [PATCH 09/17] Ruff --- src/ibex_bluesky_core/devices/simpledae/reducers.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/ibex_bluesky_core/devices/simpledae/reducers.py b/src/ibex_bluesky_core/devices/simpledae/reducers.py index b47004c..1fae379 100644 --- a/src/ibex_bluesky_core/devices/simpledae/reducers.py +++ b/src/ibex_bluesky_core/devices/simpledae/reducers.py @@ -69,7 +69,6 @@ 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() ) @@ -152,7 +151,6 @@ def __init__( 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()) ) From 5c9bde7033bf9427766fd1d2c1d523b769662953 Mon Sep 17 00:00:00 2001 From: Jack Doughty Date: Thu, 24 Oct 2024 10:12:36 +0100 Subject: [PATCH 10/17] requested changed --- .../devices/simpledae/reducers.py | 32 +++++--- tests/devices/simpledae/test_reducers.py | 73 +++++++++++++++++++ 2 files changed, 96 insertions(+), 9 deletions(-) diff --git a/src/ibex_bluesky_core/devices/simpledae/reducers.py b/src/ibex_bluesky_core/devices/simpledae/reducers.py index 1fae379..9f45c70 100644 --- a/src/ibex_bluesky_core/devices/simpledae/reducers.py +++ b/src/ibex_bluesky_core/devices/simpledae/reducers.py @@ -21,6 +21,9 @@ from ibex_bluesky_core.devices.simpledae import SimpleDae +INTENSITY_PRECISION = 6 + + async def sum_spectra(spectra: Collection[DaeSpectra]) -> sc.Variable | sc.DataArray: """Read and sum a number of spectra from the DAE. @@ -52,13 +55,15 @@ def __init__(self, prefix: str, detector_spectra: Sequence[int]) -> None: ) self.det_counts, self._det_counts_setter = soft_signal_r_and_setter(float, 0.0) - self.intensity, self._intensity_setter = soft_signal_r_and_setter(float, 0.0, precision=6) + self.intensity, self._intensity_setter = soft_signal_r_and_setter( + float, 0.0, precision=INTENSITY_PRECISION + ) self.det_counts_stddev, self._det_counts_stddev_setter = soft_signal_r_and_setter( float, 0.0 ) self.intensity_stddev, self._intensity_stddev_setter = soft_signal_r_and_setter( - float, 0.0, precision=6 + float, 0.0, precision=INTENSITY_PRECISION ) super().__init__(name="") @@ -74,12 +79,16 @@ async def reduce_data(self, dae: "SimpleDae") -> None: ) self._det_counts_setter(float(summed_counts.value)) - self._intensity_setter(float(summed_counts.value) / denominator) - detector_counts_var = 0.0 if summed_counts.variance is None else summed_counts.variance + if denominator == 0.0: + self._intensity_setter(0.0) + intensity_var = 0.0 + else: + self._intensity_setter(float(summed_counts.value) / denominator) + temp_intensity_var = (summed_counts / denominator).variance + intensity_var = temp_intensity_var if temp_intensity_var is not None else 0.0 - var_temp = (summed_counts / denominator).variance - intensity_var = var_temp if var_temp is not None else 0.0 + detector_counts_var = 0.0 if summed_counts.variance is None else summed_counts.variance self._det_counts_stddev_setter(math.sqrt(detector_counts_var)) self._intensity_stddev_setter(math.sqrt(intensity_var)) @@ -135,7 +144,9 @@ def __init__( self.det_counts, self._det_counts_setter = soft_signal_r_and_setter(float, 0.0) self.mon_counts, self._mon_counts_setter = soft_signal_r_and_setter(float, 0.0) - self.intensity, self._intensity_setter = soft_signal_r_and_setter(float, 0.0, precision=6) + self.intensity, self._intensity_setter = soft_signal_r_and_setter( + float, 0.0, precision=INTENSITY_PRECISION + ) self.det_counts_stddev, self._det_counts_stddev_setter = soft_signal_r_and_setter( float, 0.0 @@ -144,7 +155,7 @@ def __init__( float, 0.0 ) self.intensity_stddev, self._intensity_stddev_setter = soft_signal_r_and_setter( - float, 0.0, precision=6 + float, 0.0, precision=INTENSITY_PRECISION ) super().__init__(name="") @@ -161,8 +172,11 @@ async def reduce_data(self, dae: "SimpleDae") -> None: detector_counts_var = 0.0 if detector_counts.variance is None else detector_counts.variance monitor_counts_var = 0.0 if monitor_counts.variance is None else monitor_counts.variance + intensity_var = ( - 0.0 if monitor_counts_var == 0.0 else (detector_counts / monitor_counts).variance + 0.0 + if detector_counts_var + monitor_counts_var == 0.0 + else (detector_counts / monitor_counts).variance ) self._det_counts_stddev_setter(math.sqrt(detector_counts_var)) diff --git a/tests/devices/simpledae/test_reducers.py b/tests/devices/simpledae/test_reducers.py index 85f9e84..6163589 100644 --- a/tests/devices/simpledae/test_reducers.py +++ b/tests/devices/simpledae/test_reducers.py @@ -147,6 +147,42 @@ async def test_period_good_frames_normalizer_uncertainties( assert intensity_stddev == pytest.approx(math.sqrt((21000 + (123**2 / 21000)) / 123**2), 1e-4) +async def test_period_good_frames_normalizer_zero_counts( + simpledae: SimpleDae, period_good_frames_reducer: PeriodGoodFramesNormalizer +): + + period_good_frames_reducer.detectors[1].read_spectrum_dataarray = AsyncMock( + return_value=sc.DataArray( + data=sc.Variable( + dims=["tof"], + values=[0.0, 0.0, 0.0], + variances=[0.0, 0.0, 0.0], + unit=sc.units.counts, + ), + coords={"tof": sc.array(dims=["tof"], values=[0, 1, 2, 3])}, + ) + ) + period_good_frames_reducer.detectors[2].read_spectrum_dataarray = AsyncMock( + return_value=sc.DataArray( + data=sc.Variable( + dims=["tof"], + values=[0.0, 0.0, 0.0], + variances=[0.0, 0.0, 0.0], + unit=sc.units.counts, + ), + coords={"tof": sc.array(dims=["tof"], values=[0, 1, 2, 3])}, + ) + ) + + await period_good_frames_reducer.reduce_data(simpledae) + + det_counts_stddev = await period_good_frames_reducer.det_counts_stddev.get_value() + intensity_stddev = await period_good_frames_reducer.intensity_stddev.get_value() + + assert det_counts_stddev == 0 + assert intensity_stddev == 0 + + # Monitor Normalizer @@ -175,6 +211,43 @@ async def test_monitor_normalizer(simpledae: SimpleDae, monitor_normalizer: Moni assert intensity == pytest.approx(6000 / 15000) +async def test_monitor_normalizer_zero_counts( + simpledae: SimpleDae, monitor_normalizer: MonitorNormalizer +): + + monitor_normalizer.detectors[1].read_spectrum_dataarray = AsyncMock( + return_value=sc.DataArray( + data=sc.Variable( + dims=["tof"], + values=[0.0, 0.0, 0.0], + variances=[0.0, 0.0, 0.0], + unit=sc.units.counts, + ), + coords={"tof": sc.array(dims=["tof"], values=[0, 1, 2, 3])}, + ) + ) + monitor_normalizer.monitors[2].read_spectrum_dataarray = AsyncMock( + return_value=sc.DataArray( + data=sc.Variable( + dims=["tof"], + values=[0.0, 0.0, 0.0], + variances=[0.0, 0.0, 0.0], + unit=sc.units.counts, + ), + coords={"tof": sc.array(dims=["tof"], values=[0, 1, 2, 3])}, + ) + ) + + await monitor_normalizer.reduce_data(simpledae) + + det_counts_stddev = await monitor_normalizer.det_counts_stddev.get_value() + mon_counts_stddev = await monitor_normalizer.mon_counts_stddev.get_value() + intensity_stddev = await monitor_normalizer.intensity_stddev.get_value() + + assert det_counts_stddev == 0 + assert mon_counts_stddev == 0 + assert intensity_stddev == 0 + async def test_monitor_normalizer_uncertainties( simpledae: SimpleDae, monitor_normalizer: MonitorNormalizer ): From 932d32f8b01a45eff382bc893fe84a8fad1ee5d1 Mon Sep 17 00:00:00 2001 From: Jack Doughty Date: Thu, 24 Oct 2024 11:15:02 +0100 Subject: [PATCH 11/17] Ruff & requested changes --- src/ibex_bluesky_core/devices/simpledae/reducers.py | 6 +++--- tests/devices/simpledae/test_reducers.py | 6 ++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/ibex_bluesky_core/devices/simpledae/reducers.py b/src/ibex_bluesky_core/devices/simpledae/reducers.py index 9f45c70..e047ee4 100644 --- a/src/ibex_bluesky_core/devices/simpledae/reducers.py +++ b/src/ibex_bluesky_core/devices/simpledae/reducers.py @@ -84,9 +84,9 @@ async def reduce_data(self, dae: "SimpleDae") -> None: self._intensity_setter(0.0) intensity_var = 0.0 else: - self._intensity_setter(float(summed_counts.value) / denominator) - temp_intensity_var = (summed_counts / denominator).variance - intensity_var = temp_intensity_var if temp_intensity_var is not None else 0.0 + intensity = summed_counts / denominator + self._intensity_setter(intensity.value) + intensity_var = intensity.variance if intensity.variance is not None else 0.0 detector_counts_var = 0.0 if summed_counts.variance is None else summed_counts.variance diff --git a/tests/devices/simpledae/test_reducers.py b/tests/devices/simpledae/test_reducers.py index 6163589..1131bd1 100644 --- a/tests/devices/simpledae/test_reducers.py +++ b/tests/devices/simpledae/test_reducers.py @@ -3,14 +3,13 @@ import pytest import scipp as sc -from ophyd_async.core import set_mock_value - from ibex_bluesky_core.devices.simpledae import SimpleDae from ibex_bluesky_core.devices.simpledae.reducers import ( GoodFramesNormalizer, MonitorNormalizer, PeriodGoodFramesNormalizer, ) +from ophyd_async.core import set_mock_value @pytest.fixture @@ -150,7 +149,6 @@ async def test_period_good_frames_normalizer_uncertainties( async def test_period_good_frames_normalizer_zero_counts( simpledae: SimpleDae, period_good_frames_reducer: PeriodGoodFramesNormalizer ): - period_good_frames_reducer.detectors[1].read_spectrum_dataarray = AsyncMock( return_value=sc.DataArray( data=sc.Variable( @@ -214,7 +212,6 @@ async def test_monitor_normalizer(simpledae: SimpleDae, monitor_normalizer: Moni async def test_monitor_normalizer_zero_counts( simpledae: SimpleDae, monitor_normalizer: MonitorNormalizer ): - monitor_normalizer.detectors[1].read_spectrum_dataarray = AsyncMock( return_value=sc.DataArray( data=sc.Variable( @@ -248,6 +245,7 @@ async def test_monitor_normalizer_zero_counts( assert mon_counts_stddev == 0 assert intensity_stddev == 0 + async def test_monitor_normalizer_uncertainties( simpledae: SimpleDae, monitor_normalizer: MonitorNormalizer ): From edcb58472459cf077aa649a7b91765afbd68c573 Mon Sep 17 00:00:00 2001 From: Jack Doughty Date: Thu, 24 Oct 2024 11:17:37 +0100 Subject: [PATCH 12/17] ruff --- tests/devices/simpledae/test_reducers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/devices/simpledae/test_reducers.py b/tests/devices/simpledae/test_reducers.py index 1131bd1..de78479 100644 --- a/tests/devices/simpledae/test_reducers.py +++ b/tests/devices/simpledae/test_reducers.py @@ -3,13 +3,14 @@ import pytest import scipp as sc +from ophyd_async.core import set_mock_value + from ibex_bluesky_core.devices.simpledae import SimpleDae from ibex_bluesky_core.devices.simpledae.reducers import ( GoodFramesNormalizer, MonitorNormalizer, PeriodGoodFramesNormalizer, ) -from ophyd_async.core import set_mock_value @pytest.fixture From bc524e19282ba0fa79223a94089f5dd71bb2b987 Mon Sep 17 00:00:00 2001 From: Jack Doughty Date: Thu, 24 Oct 2024 11:23:48 +0100 Subject: [PATCH 13/17] Requested changes --- src/ibex_bluesky_core/devices/simpledae/reducers.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/ibex_bluesky_core/devices/simpledae/reducers.py b/src/ibex_bluesky_core/devices/simpledae/reducers.py index e047ee4..5d587fc 100644 --- a/src/ibex_bluesky_core/devices/simpledae/reducers.py +++ b/src/ibex_bluesky_core/devices/simpledae/reducers.py @@ -166,17 +166,16 @@ async def reduce_data(self, dae: "SimpleDae") -> None: sum_spectra(self.detectors.values()), sum_spectra(self.monitors.values()) ) + intensity = detector_counts / monitor_counts self._det_counts_setter(float(detector_counts.value)) self._mon_counts_setter(float(monitor_counts.value)) - self._intensity_setter(float((detector_counts / monitor_counts).value)) + self._intensity_setter(float(intensity.value)) detector_counts_var = 0.0 if detector_counts.variance is None else detector_counts.variance monitor_counts_var = 0.0 if monitor_counts.variance is None else monitor_counts.variance intensity_var = ( - 0.0 - if detector_counts_var + monitor_counts_var == 0.0 - else (detector_counts / monitor_counts).variance + 0.0 if detector_counts_var + monitor_counts_var == 0.0 else intensity.variance ) self._det_counts_stddev_setter(math.sqrt(detector_counts_var)) From 235669a6ad2a3a1822567a3168b44358f0c381d1 Mon Sep 17 00:00:00 2001 From: Jack Doughty Date: Thu, 24 Oct 2024 15:27:22 +0100 Subject: [PATCH 14/17] Requested changes --- .../devices/simpledae/reducers.py | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/ibex_bluesky_core/devices/simpledae/reducers.py b/src/ibex_bluesky_core/devices/simpledae/reducers.py index 5d587fc..fc6da0d 100644 --- a/src/ibex_bluesky_core/devices/simpledae/reducers.py +++ b/src/ibex_bluesky_core/devices/simpledae/reducers.py @@ -166,21 +166,26 @@ async def reduce_data(self, dae: "SimpleDae") -> None: sum_spectra(self.detectors.values()), sum_spectra(self.monitors.values()) ) - intensity = detector_counts / monitor_counts + if monitor_counts.value == 0.0: + self._intensity_setter(0.0) + intensity_var = 0.0 + + else: + intensity = detector_counts / monitor_counts + self._intensity_setter(float(intensity.value)) + intensity_var = intensity.variance if intensity.variance is not None else 0.0 + + self._intensity_stddev_setter(math.sqrt(intensity_var)) + self._det_counts_setter(float(detector_counts.value)) self._mon_counts_setter(float(monitor_counts.value)) - self._intensity_setter(float(intensity.value)) - + detector_counts_var = 0.0 if detector_counts.variance is None else detector_counts.variance monitor_counts_var = 0.0 if monitor_counts.variance is None else monitor_counts.variance - intensity_var = ( - 0.0 if detector_counts_var + monitor_counts_var == 0.0 else intensity.variance - ) - self._det_counts_stddev_setter(math.sqrt(detector_counts_var)) self._mon_counts_stddev_setter(math.sqrt(monitor_counts_var)) - self._intensity_stddev_setter(math.sqrt(intensity_var)) + def additional_readable_signals(self, dae: "SimpleDae") -> list[Device]: """Publish interesting signals derived or used by this reducer.""" From 5c7464d01d1c9e5d8116bcd1f7321701f1e0c991 Mon Sep 17 00:00:00 2001 From: Jack Doughty Date: Thu, 24 Oct 2024 15:27:45 +0100 Subject: [PATCH 15/17] ruff --- src/ibex_bluesky_core/devices/simpledae/reducers.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ibex_bluesky_core/devices/simpledae/reducers.py b/src/ibex_bluesky_core/devices/simpledae/reducers.py index fc6da0d..594294a 100644 --- a/src/ibex_bluesky_core/devices/simpledae/reducers.py +++ b/src/ibex_bluesky_core/devices/simpledae/reducers.py @@ -179,13 +179,12 @@ async def reduce_data(self, dae: "SimpleDae") -> None: self._det_counts_setter(float(detector_counts.value)) self._mon_counts_setter(float(monitor_counts.value)) - + detector_counts_var = 0.0 if detector_counts.variance is None else detector_counts.variance monitor_counts_var = 0.0 if monitor_counts.variance is None else monitor_counts.variance self._det_counts_stddev_setter(math.sqrt(detector_counts_var)) self._mon_counts_stddev_setter(math.sqrt(monitor_counts_var)) - def additional_readable_signals(self, dae: "SimpleDae") -> list[Device]: """Publish interesting signals derived or used by this reducer.""" From 6dcd6d1368d735fa9e2d6f617d26cd1f2b9543bc Mon Sep 17 00:00:00 2001 From: Jack Doughty <56323305+jackbdoughty@users.noreply.github.com> Date: Thu, 24 Oct 2024 15:31:22 +0100 Subject: [PATCH 16/17] Comments to reducers.py --- 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 594294a..06adfea 100644 --- a/src/ibex_bluesky_core/devices/simpledae/reducers.py +++ b/src/ibex_bluesky_core/devices/simpledae/reducers.py @@ -80,7 +80,7 @@ async def reduce_data(self, dae: "SimpleDae") -> None: self._det_counts_setter(float(summed_counts.value)) - if denominator == 0.0: + if denominator == 0.0: # To avoid zero division self._intensity_setter(0.0) intensity_var = 0.0 else: @@ -166,7 +166,7 @@ async def reduce_data(self, dae: "SimpleDae") -> None: sum_spectra(self.detectors.values()), sum_spectra(self.monitors.values()) ) - if monitor_counts.value == 0.0: + if monitor_counts.value == 0.0: # To avoid zero division self._intensity_setter(0.0) intensity_var = 0.0 From e426a43e62d8db71a28162717daf89ca401aa7d5 Mon Sep 17 00:00:00 2001 From: Jack Harper Date: Thu, 24 Oct 2024 15:34:33 +0100 Subject: [PATCH 17/17] ruff --- 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 06adfea..4abe0a8 100644 --- a/src/ibex_bluesky_core/devices/simpledae/reducers.py +++ b/src/ibex_bluesky_core/devices/simpledae/reducers.py @@ -80,7 +80,7 @@ async def reduce_data(self, dae: "SimpleDae") -> None: self._det_counts_setter(float(summed_counts.value)) - if denominator == 0.0: # To avoid zero division + if denominator == 0.0: # To avoid zero division self._intensity_setter(0.0) intensity_var = 0.0 else: @@ -166,7 +166,7 @@ async def reduce_data(self, dae: "SimpleDae") -> None: sum_spectra(self.detectors.values()), sum_spectra(self.monitors.values()) ) - if monitor_counts.value == 0.0: # To avoid zero division + if monitor_counts.value == 0.0: # To avoid zero division self._intensity_setter(0.0) intensity_var = 0.0