Skip to content

Commit c3bca10

Browse files
authored
Merge pull request #83 from ISISComputingGroup/uncertainties
Set uncertainties more correctly
2 parents fc08217 + 07a8b6c commit c3bca10

File tree

5 files changed

+93
-69
lines changed

5 files changed

+93
-69
lines changed

src/ibex_bluesky_core/callbacks/reflectometry/_det_map.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from matplotlib.axes import Axes
3434

3535
from ibex_bluesky_core.callbacks.plotting import show_plot
36+
from ibex_bluesky_core.devices.simpledae.reducers import VARIANCE_ADDITION
3637

3738
logger = logging.getLogger(__name__)
3839

@@ -60,23 +61,23 @@ def event(self, doc: Event, **kwargs: dict[str, Any]) -> Event:
6061
det_data = doc["data"][self._det_name]
6162
mon_data = doc["data"][self._mon_name]
6263

63-
det = sc.Variable(
64-
dims=["spectrum"], values=det_data, variances=det_data + 0.5, dtype="float64"
65-
)
66-
mon = sc.Variable(
67-
dims=["spectrum"], values=mon_data, variances=mon_data + 0.5, dtype="float64"
68-
)
64+
det = sc.Variable(dims=["spectrum"], values=det_data, variances=det_data, dtype="float64")
65+
mon = sc.Variable(dims=["spectrum"], values=mon_data, variances=mon_data, dtype="float64")
6966

7067
det_sum = det.sum()
7168
mon_sum = mon.sum()
7269

70+
# See doc\architectural_decisions\005-variance-addition.md
71+
# for justification of this addition to variances.
72+
det_sum.variance += VARIANCE_ADDITION
73+
7374
if mon_sum.value == 0.0:
7475
raise ValueError(
7576
"No monitor counts. Check beamline setup & beam status. "
7677
"I/I_0 normalization not possible."
7778
)
78-
else:
79-
normalized = det_sum / mon_sum
79+
80+
normalized = det_sum / mon_sum
8081

8182
doc["data"][self._out_name] = normalized.value
8283
doc["data"][self._out_name + "_err"] = math.sqrt(normalized.variance)

src/ibex_bluesky_core/devices/dae/dae_spectra.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
from ophyd_async.core import Array1D, SignalR, StandardReadable
1111
from ophyd_async.epics.core import epics_signal_r
1212

13-
VARIANCE_ADDITION = 0.5
1413
logger = logging.getLogger(__name__)
1514

1615

@@ -116,14 +115,11 @@ async def read_spectrum_dataarray(self) -> sc.DataArray:
116115
if unit is None:
117116
raise ValueError("Could not determine engineering units of tof edges.")
118117

119-
# See doc\architectural_decisions\005-variance-addition.md
120-
# for justfication of the VARIANCE_ADDITION to variances
121-
122118
return sc.DataArray(
123119
data=sc.Variable(
124120
dims=["tof"],
125121
values=counts,
126-
variances=counts + VARIANCE_ADDITION,
122+
variances=counts,
127123
unit=sc.units.counts,
128124
dtype="float64",
129125
),

src/ibex_bluesky_core/devices/simpledae/reducers.py

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030

3131

3232
INTENSITY_PRECISION = 6
33+
VARIANCE_ADDITION = 0.5
3334

3435

3536
async def sum_spectra(spectra: Collection[DaeSpectra]) -> sc.Variable | sc.DataArray:
@@ -176,20 +177,21 @@ async def reduce_data(self, dae: "SimpleDae") -> None:
176177
self.sum_detector(self.detectors.values()), self.denominator(dae).get_value()
177178
)
178179

179-
self._det_counts_setter(float(summed_counts.value))
180+
if denominator == 0.0:
181+
raise ValueError("Cannot normalize; denominator is zero. Check beamline configuration.")
182+
183+
# See doc\architectural_decisions\005-variance-addition.md
184+
# for justification of this addition to variances.
185+
summed_counts.variance += VARIANCE_ADDITION
180186

181-
if denominator == 0.0: # To avoid zero division
182-
self._intensity_setter(0.0)
183-
intensity_var = 0.0
184-
else:
185-
intensity = summed_counts / denominator
186-
self._intensity_setter(intensity.value)
187-
intensity_var = intensity.variance if intensity.variance is not None else 0.0
187+
intensity = summed_counts / denominator
188188

189-
detector_counts_var = 0.0 if summed_counts.variance is None else summed_counts.variance
189+
self._det_counts_setter(float(summed_counts.value))
190+
self._det_counts_stddev_setter(math.sqrt(summed_counts.variance))
191+
192+
self._intensity_setter(intensity.value)
193+
self._intensity_stddev_setter(math.sqrt(intensity.variance))
190194

191-
self._det_counts_stddev_setter(math.sqrt(detector_counts_var))
192-
self._intensity_stddev_setter(math.sqrt(intensity_var))
193195
logger.info("reduction complete")
194196

195197
def additional_readable_signals(self, dae: "SimpleDae") -> list[Device]:
@@ -284,25 +286,25 @@ async def reduce_data(self, dae: "SimpleDae") -> None:
284286
self.sum_monitor(self.monitors.values()),
285287
)
286288

287-
if monitor_counts.value == 0.0: # To avoid zero division
288-
self._intensity_setter(0.0)
289-
intensity_var = 0.0
289+
if monitor_counts.value == 0.0:
290+
raise ValueError(
291+
"Cannot normalize; got zero monitor counts. Check beamline configuration."
292+
)
290293

291-
else:
292-
intensity = detector_counts / monitor_counts
293-
self._intensity_setter(float(intensity.value))
294-
intensity_var = intensity.variance if intensity.variance is not None else 0.0
294+
# See doc\architectural_decisions\005-variance-addition.md
295+
# for justification of this addition to variances.
296+
detector_counts.variance += VARIANCE_ADDITION
295297

296-
self._intensity_stddev_setter(math.sqrt(intensity_var))
298+
intensity = detector_counts / monitor_counts
297299

300+
self._intensity_setter(float(intensity.value))
298301
self._det_counts_setter(float(detector_counts.value))
299302
self._mon_counts_setter(float(monitor_counts.value))
300303

301-
detector_counts_var = 0.0 if detector_counts.variance is None else detector_counts.variance
302-
monitor_counts_var = 0.0 if monitor_counts.variance is None else monitor_counts.variance
304+
self._intensity_stddev_setter(math.sqrt(intensity.variance))
305+
self._det_counts_stddev_setter(math.sqrt(detector_counts.variance))
306+
self._mon_counts_stddev_setter(math.sqrt(monitor_counts.variance))
303307

304-
self._det_counts_stddev_setter(math.sqrt(detector_counts_var))
305-
self._mon_counts_stddev_setter(math.sqrt(monitor_counts_var))
306308
logger.info("reduction complete")
307309

308310
def additional_readable_signals(self, dae: "SimpleDae") -> list[Device]:

tests/devices/simpledae/test_reducers.py

Lines changed: 50 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# pyright: reportMissingParameterType=false
22
import math
3+
import re
34
from unittest.mock import AsyncMock
45

56
import numpy as np
@@ -9,6 +10,7 @@
910

1011
from ibex_bluesky_core.devices.simpledae import SimpleDae
1112
from ibex_bluesky_core.devices.simpledae.reducers import (
13+
VARIANCE_ADDITION,
1214
GoodFramesNormalizer,
1315
MonitorNormalizer,
1416
PeriodGoodFramesNormalizer,
@@ -256,6 +258,7 @@ def spectra_bins_easy_to_test() -> sc.DataArray:
256258
data=sc.Variable(
257259
dims=["tof"],
258260
values=[1000.0, 2000.0, 3000.0, 2000.0, 1000.0],
261+
variances=[1000.0, 2000.0, 3000.0, 2000.0, 1000.0],
259262
unit=sc.units.counts,
260263
dtype="float64",
261264
),
@@ -273,6 +276,7 @@ def spectra_bins_tof_convert_to_wavelength_easy() -> sc.DataArray:
273276
data=sc.Variable(
274277
dims=["tof"],
275278
values=[1000.0, 2000.0, 3000.0, 2000.0, 1000.0],
279+
variances=[1000.0, 2000.0, 3000.0, 2000.0, 1000.0],
276280
unit=sc.units.counts,
277281
dtype="float64",
278282
),
@@ -293,6 +297,7 @@ def spectra_bins_tof_convert_to_wavelength_easy_copy() -> sc.DataArray:
293297
data=sc.Variable(
294298
dims=["tof"],
295299
values=[1000.0, 2000.0, 3000.0, 2000.0, 1000.0],
300+
variances=[1000.0, 2000.0, 3000.0, 2000.0, 1000.0],
296301
unit=sc.units.counts,
297302
dtype="float64",
298303
),
@@ -361,13 +366,23 @@ async def test_period_good_frames_normalizer(
361366

362367
period_good_frames_reducer.detectors[1].read_spectrum_dataarray = AsyncMock(
363368
return_value=sc.DataArray(
364-
data=sc.Variable(dims=["tof"], values=[1000.0, 2000.0, 3000.0], unit=sc.units.counts),
369+
data=sc.Variable(
370+
dims=["tof"],
371+
values=[1000.0, 2000.0, 3000.0],
372+
variances=[1000.0, 2000.0, 3000.0],
373+
unit=sc.units.counts,
374+
),
365375
coords={"tof": sc.array(dims=["tof"], values=[0, 1, 2, 3])},
366376
)
367377
)
368378
period_good_frames_reducer.detectors[2].read_spectrum_dataarray = AsyncMock(
369379
return_value=sc.DataArray(
370-
data=sc.Variable(dims=["tof"], values=[4000.0, 5000.0, 6000.0], unit=sc.units.counts),
380+
data=sc.Variable(
381+
dims=["tof"],
382+
values=[4000.0, 5000.0, 6000.0],
383+
variances=[1000.0, 2000.0, 3000.0],
384+
unit=sc.units.counts,
385+
),
371386
coords={"tof": sc.array(dims=["tof"], values=[0, 1, 2, 3])},
372387
)
373388
)
@@ -395,6 +410,7 @@ async def test_period_good_frames_normalizer_uncertainties(
395410
values=[1000.0, 2000.0, 3000.0],
396411
variances=[1000.0, 2000.0, 3000.0],
397412
unit=sc.units.counts,
413+
dtype="float64",
398414
),
399415
coords={"tof": sc.array(dims=["tof"], values=[0, 1, 2, 3])},
400416
)
@@ -406,6 +422,7 @@ async def test_period_good_frames_normalizer_uncertainties(
406422
values=[4000.0, 5000.0, 6000.0],
407423
variances=[4000.0, 5000.0, 6000.0],
408424
unit=sc.units.counts,
425+
dtype="float64",
409426
),
410427
coords={
411428
"tof": sc.array(
@@ -420,8 +437,10 @@ async def test_period_good_frames_normalizer_uncertainties(
420437
det_counts_stddev = await period_good_frames_reducer.det_counts_stddev.get_value()
421438
intensity_stddev = await period_good_frames_reducer.intensity_stddev.get_value()
422439

423-
assert det_counts_stddev == math.sqrt(21000)
424-
assert intensity_stddev == pytest.approx(math.sqrt((21000 + (123**2 / 21000)) / 123**2), 1e-4)
440+
assert det_counts_stddev == math.sqrt(21000 + VARIANCE_ADDITION)
441+
assert intensity_stddev == pytest.approx(
442+
math.sqrt((21000 + VARIANCE_ADDITION) / (123**2)), 1e-4
443+
)
425444

426445

427446
async def test_period_good_frames_normalizer_zero_counts(
@@ -458,13 +477,11 @@ async def test_period_good_frames_normalizer_zero_counts(
458477
)
459478
)
460479

461-
await period_good_frames_reducer.reduce_data(simpledae)
462-
463-
det_counts_stddev = await period_good_frames_reducer.det_counts_stddev.get_value()
464-
intensity_stddev = await period_good_frames_reducer.intensity_stddev.get_value()
465-
466-
assert det_counts_stddev == 0
467-
assert intensity_stddev == 0
480+
with pytest.raises(
481+
ValueError,
482+
match=re.escape("Cannot normalize; denominator is zero. Check beamline configuration."),
483+
):
484+
await period_good_frames_reducer.reduce_data(simpledae)
468485

469486

470487
async def test_scalar_normalizer_tof_bounded_zero_to_one_half(
@@ -542,13 +559,23 @@ async def test_monitor_normalizer(
542559
): # 1, 1
543560
monitor_normalizer.detectors[1].read_spectrum_dataarray = AsyncMock(
544561
return_value=sc.DataArray(
545-
data=sc.Variable(dims=["tof"], values=[1000.0, 2000.0, 3000.0], unit=sc.units.counts),
562+
data=sc.Variable(
563+
dims=["tof"],
564+
values=[1000.0, 2000.0, 3000.0],
565+
variances=[1000.0, 2000.0, 3000.0],
566+
unit=sc.units.counts,
567+
),
546568
coords={"tof": sc.array(dims=["tof"], values=[0, 1, 2, 3])},
547569
)
548570
)
549571
monitor_normalizer.monitors[2].read_spectrum_dataarray = AsyncMock(
550572
return_value=sc.DataArray(
551-
data=sc.Variable(dims=["tof"], values=[4000.0, 5000.0, 6000.0], unit=sc.units.counts),
573+
data=sc.Variable(
574+
dims=["tof"],
575+
values=[4000.0, 5000.0, 6000.0],
576+
variances=[4000.0, 5000.0, 6000.0],
577+
unit=sc.units.counts,
578+
),
552579
coords={"tof": sc.array(dims=["tof"], values=[0, 1, 2, 3])},
553580
)
554581
)
@@ -590,15 +617,11 @@ async def test_monitor_normalizer_zero_counts(
590617
)
591618
)
592619

593-
await monitor_normalizer.reduce_data(simpledae)
594-
595-
det_counts_stddev = await monitor_normalizer.det_counts_stddev.get_value()
596-
mon_counts_stddev = await monitor_normalizer.mon_counts_stddev.get_value()
597-
intensity_stddev = await monitor_normalizer.intensity_stddev.get_value()
598-
599-
assert det_counts_stddev == 0
600-
assert mon_counts_stddev == 0
601-
assert intensity_stddev == 0
620+
with pytest.raises(
621+
ValueError,
622+
match=re.escape("Cannot normalize; got zero monitor counts. Check beamline configuration."),
623+
):
624+
await monitor_normalizer.reduce_data(simpledae)
602625

603626

604627
async def test_monitor_normalizer_uncertainties(
@@ -633,9 +656,11 @@ async def test_monitor_normalizer_uncertainties(
633656
mon_counts_stddev = await monitor_normalizer.mon_counts_stddev.get_value()
634657
intensity_stddev = await monitor_normalizer.intensity_stddev.get_value()
635658

636-
assert det_counts_stddev == math.sqrt(6000)
659+
assert det_counts_stddev == math.sqrt(6000 + VARIANCE_ADDITION)
637660
assert mon_counts_stddev == math.sqrt(15000)
638-
assert intensity_stddev == pytest.approx(math.sqrt((6000 + (6000**2 / 15000)) / 15000**2), 1e-4)
661+
assert intensity_stddev == pytest.approx(
662+
(6000 / 15000) * math.sqrt((6000.5 / 6000**2) + (15000 / 15000**2)), 1e-8
663+
)
639664

640665

641666
def test_monitor_normalizer_publishes_raw_and_normalized_counts(
@@ -658,7 +683,7 @@ def test_monitor_normalizer_publishes_raw_and_normalized_count_uncertainties(
658683
assert monitor_normalizer.mon_counts_stddev in readables
659684

660685

661-
async def test_monitor_normalizer_det_sum_normal_mon_sum_tof_bound_( # 1, 2
686+
async def test_monitor_normalizer_det_sum_normal_mon_sum_tof_bound( # 1, 2
662687
simpledae: SimpleDae,
663688
monitor_normalizer_zero_to_one_half_det_norm_mon_tof: MonitorNormalizer,
664689
spectra_bins_easy_to_test: sc.DataArray,

tests/devices/test_dae.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,23 @@
1212
from ophyd_async.testing import get_mock_put, set_mock_value
1313

1414
from ibex_bluesky_core.devices import compress_and_hex, dehex_and_decompress
15+
from ibex_bluesky_core.devices.dae import convert_xml_to_names_and_values, set_value_in_dae_xml
1516
from ibex_bluesky_core.devices.dae.dae import Dae, RunstateEnum
17+
from ibex_bluesky_core.devices.dae.dae_controls import BeginRunExBits
1618
from ibex_bluesky_core.devices.dae.dae_period_settings import (
1719
DaePeriodSettings,
1820
DaePeriodSettingsData,
1921
PeriodSource,
2022
PeriodType,
2123
SinglePeriodSettings,
24+
_convert_period_settings_to_xml,
2225
)
2326
from ibex_bluesky_core.devices.dae.dae_settings import (
2427
DaeSettings,
2528
DaeSettingsData,
2629
TimingSource,
2730
)
28-
from ibex_bluesky_core.devices.dae.dae_spectra import VARIANCE_ADDITION, DaeSpectra
31+
from ibex_bluesky_core.devices.dae.dae_spectra import DaeSpectra
2932
from ibex_bluesky_core.devices.dae.dae_tcb_settings import (
3033
CalculationMethod,
3134
DaeTCBSettings,
@@ -34,11 +37,8 @@
3437
TimeRegimeMode,
3538
TimeRegimeRow,
3639
TimeUnit,
40+
_convert_tcb_settings_to_xml,
3741
)
38-
from src.ibex_bluesky_core.devices.dae import convert_xml_to_names_and_values, set_value_in_dae_xml
39-
from src.ibex_bluesky_core.devices.dae.dae_controls import BeginRunExBits
40-
from src.ibex_bluesky_core.devices.dae.dae_period_settings import _convert_period_settings_to_xml
41-
from src.ibex_bluesky_core.devices.dae.dae_tcb_settings import _convert_tcb_settings_to_xml
4242
from tests.conftest import MOCK_PREFIX
4343
from tests.devices.dae_testing_data import (
4444
dae_settings_template,
@@ -980,9 +980,9 @@ async def test_read_spectrum_dataarray(spectrum: DaeSpectra):
980980
dims=["tof"],
981981
values=[1000, 2000, 3000],
982982
variances=[
983-
1000 + VARIANCE_ADDITION,
984-
2000 + VARIANCE_ADDITION,
985-
3000 + VARIANCE_ADDITION,
983+
1000,
984+
2000,
985+
3000,
986986
],
987987
unit=sc.units.counts,
988988
dtype="float32",

0 commit comments

Comments
 (0)