Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ticket42 sum detect monitor spectra #66

Open
wants to merge 27 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
794bf74
Some tests for tof bounding of spectra
esmith1729 Nov 15, 2024
e90c0a3
Adding explicit declaration of data type
esmith1729 Nov 15, 2024
9cc4810
Add new summation for tof bounding of spectra
esmith1729 Nov 15, 2024
10edc74
Add tof_bounded_spectra functionality to monitor normalizer as well, …
esmith1729 Nov 19, 2024
7f30bc1
Add scippneutron to dependencies
esmith1729 Nov 21, 2024
2f386a5
Add wavelength_bounded_spectra method to convert from tof to wavelength
esmith1729 Nov 21, 2024
a373827
ruff formatting
esmith1729 Nov 21, 2024
873452e
ruff formatting, add fixtures and tests for monitor normalizer for to…
esmith1729 Nov 21, 2024
80bba94
change dtype of tof variable, as it's explicitly float64 in dae_spect…
esmith1729 Nov 21, 2024
d4a6cf0
Add tests to cover case of missing tof in dimensions of bounds parame…
esmith1729 Nov 22, 2024
35baa3b
Merge remote-tracking branch 'origin/main' into ticket42_sum_detect_m…
esmith1729 Nov 25, 2024
9c51a75
ruff changes
esmith1729 Nov 25, 2024
baf675d
Ruff formatting, sphinx documentation changes
esmith1729 Nov 25, 2024
20d239d
Make class comments more concise and understandable.
esmith1729 Nov 27, 2024
85c9b2e
Add empty line to create bulleted list in API reference docs
esmith1729 Nov 27, 2024
849ab24
Rename detector_summer and monitor_summer to sum_detector and sum_mon…
esmith1729 Nov 27, 2024
4e97f7f
documentation on time of flight and wavelength bounding spectra.
esmith1729 Nov 27, 2024
b4dd536
Removed trailing whitespace
esmith1729 Nov 27, 2024
23454e1
Merge branch 'main' into ticket42_sum_detect_monitor_spectra
esmith1729 Dec 11, 2024
35906a7
Some rewording fixes, and adding more API references.
esmith1729 Dec 12, 2024
62e90a8
rename beam_total to total_flight_path_length, add test for wavelengt…
esmith1729 Dec 13, 2024
9294e97
merge changes with imports,
esmith1729 Dec 13, 2024
1534799
ruff change import Awaitable from collections.abc
esmith1729 Dec 13, 2024
1fa1c9c
Fix pyobj path reference for PeriodGoodFramesNormalizer
esmith1729 Dec 13, 2024
34d9b58
fix path for py object
esmith1729 Dec 13, 2024
190e383
fix py obj path
esmith1729 Dec 13, 2024
f02a014
Add explanation about assuming pixels share the same flight path length
esmith1729 Dec 16, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions doc/devices/dae.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,58 @@ 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
esmith1729 marked this conversation as resolved.
Show resolved Hide resolved
summing function which can optionally sum spectra between provided time of flight or wavelength bounds.

{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:
```
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. 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

wavelength_bounds = scipp.array(dims=["tof"], values=[0.0, 5.1], unit=scipp.units.angstrom, dtype="float64")
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, total_flight_path_length),
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 summing over the entire spectrum.


## Waiters

A `waiter` defines an arbitrary strategy for how long to count at each point.
Expand Down Expand Up @@ -362,3 +414,17 @@ 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 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
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 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 bounds.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ dependencies = [
"numpy",
"scipp",
"tzdata",
"scippneutron",
]

[project.optional-dependencies]
Expand Down
5 changes: 4 additions & 1 deletion src/ibex_bluesky_core/devices/dae/dae_spectra.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ async def read_spectrum_dataarray(self) -> sc.DataArray:
values=counts,
variances=counts + VARIANCE_ADDITION,
unit=sc.units.counts,
dtype="float64",
),
coords={"tof": sc.array(dims=["tof"], values=tof_edges, unit=sc.Unit(unit))},
coords={
"tof": sc.array(dims=["tof"], values=tof_edges, unit=sc.Unit(unit), dtype="float64")
},
)
118 changes: 112 additions & 6 deletions src/ibex_bluesky_core/devices/simpledae/reducers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
import logging
import math
from abc import ABC, abstractmethod
from collections.abc import Collection, Sequence
from typing import TYPE_CHECKING
from collections.abc import Awaitable, Collection, Sequence
from typing import TYPE_CHECKING, Callable

import scipp as sc
from ophyd_async.core import (
Expand All @@ -15,6 +15,7 @@
StandardReadable,
soft_signal_r_and_setter,
)
from scippneutron import conversion

from ibex_bluesky_core.devices.dae.dae_spectra import DaeSpectra
from ibex_bluesky_core.devices.simpledae.strategies import Reducer
Expand All @@ -33,6 +34,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")
Expand All @@ -42,15 +45,99 @@ 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]]:
"""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

"""
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_tof(spectra: Collection[DaeSpectra]) -> sc.Variable | sc.DataArray:
"""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
summed_counts += tof_bound_spectra.rebin({"tof": bounds}).sum()
return summed_counts

return sum_spectra_with_tof


def wavelength_bounded_spectra(
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 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.
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:
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.

"""
bounds_value = 2

if "tof" not in bounds.dims:
raise ValueError("Should contain tof dims")
esmith1729 marked this conversation as resolved.
Show resolved Hide resolved
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 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=total_flight_path_length
)
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, ABC):
"""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],
sum_detector: Callable[
[Collection[DaeSpectra]], Awaitable[sc.Variable | sc.DataArray]
] = sum_spectra,
) -> None:
"""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.
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(
Expand All @@ -71,6 +158,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.sum_detector = sum_detector

super().__init__(name="")

Expand All @@ -82,7 +170,7 @@ async def reduce_data(self, dae: "SimpleDae") -> None:
"""Apply the normalization."""
logger.info("starting reduction")
summed_counts, denominator = await asyncio.gather(
sum_spectra(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))
Expand Down Expand Up @@ -132,14 +220,29 @@ 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],
sum_detector: Callable[
[Collection[DaeSpectra]], Awaitable[sc.Variable | sc.DataArray]
] = sum_spectra,
sum_monitor: Callable[
[Collection[DaeSpectra]], Awaitable[sc.Variable | sc.DataArray]
] = sum_spectra,
) -> None:
"""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.
monitor_spectra: a sequence of spectra number (monitors) to sum and normalize by.
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:"
Expand All @@ -165,14 +268,17 @@ def __init__(
self.intensity_stddev, self._intensity_stddev_setter = soft_signal_r_and_setter(
float, 0.0, precision=INTENSITY_PRECISION
)
self.sum_detector = sum_detector
self.sum_monitor = sum_monitor

super().__init__(name="")

async def reduce_data(self, dae: "SimpleDae") -> None:
"""Apply the normalization."""
logger.info("starting reduction")
detector_counts, monitor_counts = await asyncio.gather(
sum_spectra(self.detectors.values()), sum_spectra(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
Expand Down
1 change: 1 addition & 0 deletions src/ibex_bluesky_core/run_engine/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
"""
Expand Down
Loading
Loading