Skip to content
Merged
Show file tree
Hide file tree
Changes from 66 commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
39073dd
start implementing PolarisingDae
jackbdoughty May 2, 2025
1c240cd
Merge remote-tracking branch 'origin/58_polarization_asymmetry' into …
jackbdoughty May 2, 2025
4b9a428
add support for multiple wavelength bands
jackbdoughty May 2, 2025
ace2803
dae implemented pre test
jackbdoughty May 7, 2025
361b034
update flipper signal red
jackbdoughty May 7, 2025
f09fe6e
update flipper signal ref
jackbdoughty May 7, 2025
5d77efe
refactoring & fixes post test
jackbdoughty May 7, 2025
0096df7
refactoring
jackbdoughty May 8, 2025
8d82f8a
polarising dae unit tests
jackbdoughty May 8, 2025
f172c04
i <3 unit tests
jackbdoughty May 8, 2025
a37539a
unit tests 4eva
jackbdoughty May 9, 2025
59b5d47
passing tests full coverage
jackbdoughty May 12, 2025
823d209
unit tests refactoring
jackbdoughty May 12, 2025
f6d824d
update docs
jackbdoughty May 12, 2025
71cdd84
docstring improvement
jackbdoughty May 15, 2025
7321afc
docstring improvement
jackbdoughty May 15, 2025
0202021
removing unused imports
jackbdoughty May 15, 2025
c71f6b6
docs fix
jackbdoughty May 15, 2025
8b8ccc2
docs fix
jackbdoughty May 15, 2025
400b2e1
docs fix
jackbdoughty May 15, 2025
0ae86be
ruff fixes
jackbdoughty May 15, 2025
aae36c5
ruff fixes
jackbdoughty May 15, 2025
c646c76
ruff fixes
jackbdoughty May 15, 2025
2fcde46
ruff fixes
jackbdoughty May 15, 2025
70a6913
ruff fixes
jackbdoughty May 15, 2025
2bddf69
ruff fixes
jackbdoughty May 15, 2025
4ac9d46
circular import
jackbdoughty May 15, 2025
496768e
initial commit
jackbdoughty May 15, 2025
c71118d
ruff fixes
jackbdoughty May 15, 2025
2354174
ruff fixes
jackbdoughty May 15, 2025
8984d0c
ruff & pyright
jackbdoughty May 15, 2025
c8926ca
ruff & pyright & refactoring
jackbdoughty May 16, 2025
64b3395
refactoring & backwards compatability
jackbdoughty May 16, 2025
1982fbd
refactor
jackbdoughty May 16, 2025
660be7a
simpledae refactor
jackbdoughty May 20, 2025
ede3a27
ruff
jackbdoughty May 20, 2025
9567f4f
docs fix
jackbdoughty May 20, 2025
0713316
Merge branch 'main' into Ticket_125
jackbdoughty May 20, 2025
b312d98
unit tests refactor
jackbdoughty May 20, 2025
1931733
docs
jackbdoughty May 20, 2025
988788b
Merge branch 'main' into Ticket_125
jackbdoughty Jun 2, 2025
3007d6c
unit tests fix
jackbdoughty Jun 2, 2025
8ac1353
refactoring & chained live fit
jackbdoughty Jun 13, 2025
1faa3e7
Merge branch 'main' into Ticket_125
jackbdoughty Jun 13, 2025
827ee6e
ruff stuff
jackbdoughty Jun 13, 2025
bc3c6fb
Merge branch 'Ticket_125' of https://github.com/ISISComputingGroup/ib…
jackbdoughty Jun 13, 2025
87d73d8
pyright
jackbdoughty Jun 13, 2025
5712e30
ruffff
jackbdoughty Jun 13, 2025
2035b1e
unit test fix
jackbdoughty Jun 13, 2025
d34142f
chainedlivefit unit tests
jackbdoughty Jun 17, 2025
08cf24c
ruff & pyright
jackbdoughty Jun 17, 2025
97aebbf
Merge branch 'main' into Ticket_125
jackbdoughty Jun 17, 2025
6bca42c
remove a bit of code that idk where it came from?
jackbdoughty Jun 17, 2025
71ca7d9
add getters for lf&lfp
jackbdoughty Jun 17, 2025
d87d47a
refactoring & fixes
jackbdoughty Jun 18, 2025
45d4dff
requested changes
jackbdoughty Jun 18, 2025
9abd9af
refactoring
jackbdoughty Jun 19, 2025
a089186
ruff unhappy now happy
jackbdoughty Jun 19, 2025
472329b
docs changes
jackbdoughty Jun 19, 2025
b1de387
Merge branch 'main' into Ticket_125
jackbdoughty Jun 19, 2025
bf062a7
polarisation consolidation transformation
jackbdoughty Jun 19, 2025
419f639
refactoring
jackbdoughty Jun 20, 2025
fd9f2a8
typing changes
jackbdoughty Jun 24, 2025
3e5ca92
add comment to chainedlivefit
jackbdoughty Jun 24, 2025
32edac4
Merge branch 'main' into Ticket_125
jackbdoughty Jun 25, 2025
52c6d76
tiny description change
jackbdoughty Jun 26, 2025
7c19456
Merge branch 'main' into Ticket_125
jackbdoughty Jun 27, 2025
2ba27dd
Merge branch 'main' into Ticket_125
jackbdoughty Jul 1, 2025
b8e00da
requested changed
jackbdoughty Jul 1, 2025
8852fc0
docs fix
jackbdoughty Jul 1, 2025
8807bdb
docs fix again
jackbdoughty Jul 1, 2025
eef347c
remove show plot
jackbdoughty Jul 2, 2025
ff06540
docs change
jackbdoughty Jul 2, 2025
e48932b
Merge branch 'main' into Ticket_125
jackbdoughty Jul 2, 2025
b454b5a
fix merge
jackbdoughty Jul 2, 2025
40944ff
ruff pyright
jackbdoughty Jul 2, 2025
b61f1d7
change flipper to be generic
jackbdoughty Jul 2, 2025
ff10f33
flipper names
jackbdoughty Jul 2, 2025
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
104 changes: 101 additions & 3 deletions doc/devices/dae.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,29 @@ as a list, and `units` (μs/microseconds for time of flight bounding, and angstr

If you don't specify either of these options, they will default to summing over the entire spectrum.

### Polarization/Asymmetry

Polarization refers to the property of transverse waves which specifies the geometrical orientation of the
oscillations.

The polarization funtion provided will calculate the polarization between two values, A and B, which
have different definitions based on the instrument context.

Instrument-Specific Interpretations
SANS Instruments (e.g., LARMOR)
A: Intensity in DAE period before switching a flipper.
B: Intensity in DAE period after switching a flipper.

Reflectometry Instruments (e.g., POLREF)
Similar to LARMOR, A and B represent intensities before and after flipper switching.

Muon Instruments
A and B refer to Measurements from different detector banks.

{py:obj}`ibex_bluesky_core.devices.simpledae.polarisingdae.polarization`

See [`PolarisingDae`](#PolarisingDae) and [`PolarisingReducer`](#PolarisingReducer) for how this is integrated into DAE behaviour.

## Waiters

A [`waiter`](ibex_bluesky_core.devices.simpledae.Waiter) defines an arbitrary strategy for how long to count at each point.
Expand Down Expand Up @@ -347,6 +370,62 @@ Waits for a user-specified time duration, irrespective of DAE state.

Does not publish any additional signals.

## Polarising DAE

The polarising DAE provides specialised functionality for taking data whilst taking into account the polarity of the beam.

### PolarisingDae

[`PolarisingDae`](ibex_bluesky_core.devices.simpledae.polarisingdae.PolarisingDae) is a more complex version of `SimpleDae`, designed specifically for taking polarisation measurements. It requires a flipper device and uses it to flip from one neutron state to the other between runs.

Key features:
- Controls a flipper device to switch between neutron states
- Handles two separate reduction strategies (up and down states)
- Calculates polarisation from the two states after the two runs

### polarising_dae

[`polarising_dae`](ibex_bluesky_core.devices.simpledae.polarisingdae.polarising_dae) is a helper function that creates a configured `PolarisingDae` instance with wavelength binning based normalisation and polarisation calculation capabilities.

The following is how you may want to use `polarising_dae`:
```python
import scipp

flipper = block_rw(float, "alice")
wavelength_interval = scipp.array(dims=["tof"], values=[0, 9999999999.0], unit=scipp.units.angstrom, dtype="float64") # Creates a wavelength interval of the whole sprectrum
total_flight_path_length = sc.scalar(value=10, unit=sc.units.m)

dae = polarising_dae(det_pixels=[1], frames=500, flipper=flipper, flipper_states=(0.0, 1.0), intervals=[wavelength_interval], total_flight_path_length=total_flight_path_length, monitor=2)
```

:::{note}
Notice how you must define what the `flipper_states` are to the polarising dae. This is so that it knows what to assign to the `flipper` device to move it to the "up state" and "down state"
.
:::

### Polarising Reducers

#### WavelengthBoundedNormalizer

[`WavelengthBoundedNormalizer`](ibex_bluesky_core.devices.simpledae.polarisingdae.WavelengthBoundedNormalizer) sums wavelength-bounded spectra and normalises by monitor intensity.

Published signals:
- `wavelength_bands`: DeviceVector containing wavelength band measurements
- `det_counts`: detector counts in the wavelength band
- `mon_counts`: monitor counts in the wavelength band
- `intensity`: normalised intensity in the wavelength band
- Associated uncertainty measurements for each value

#### PolarisingReducer

[`PolarisingReducer`](ibex_bluesky_core.devices.simpledae.polarisingdae.PolarisingReducer) calculates polarisation from 'spin-up' and 'spin-down' states of a polarising DAE. Uses the [`polarization`](#polarizationasymmetry) algorithm.

Published signals:
- `wavelength_bands`: DeviceVector containing polarisation measurements
- `polarisation`: The calculated polarisation value for that wavelength band
- `polarisation_ratio`: Ratio between up and down states for that wavelength band
- Associated uncertainty measurements for each value

---

## `Dae` (base class, advanced)
Expand Down Expand Up @@ -465,9 +544,6 @@ A [`DaeSpectra`](ibex_bluesky_core.devices.dae.DaeSpectra) object provides 3 arr
The [`Dae`](ibex_bluesky_core.devices.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
Expand All @@ -478,3 +554,25 @@ or wavelength 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.

### Wavelength Band Classes

#### WavelengthBand

[`WavelengthBand`](ibex_bluesky_core.devices.dae.WavelengthBand) represents a few measurements within a specific wavelength band. Has a setter method to assign values to the published signals.

Additional Signals:
- `det_counts`: Detector counts
- `mon_counts`: Monitor counts
- `intensity`: Normalized intensity
- Associated uncertainty measurements for each value

#### PolarisedWavelengthBand

[`PolarisedWavelengthBand`](ibex_bluesky_core.devices.dae.PolarisedWavelengthBand) represents the polarisation information calculated using measurements taken from two `WavelengthBand` objects, one published from an "up state" `WavelengthBoundedNormalizer`
and the other from a "down state" `WavelengthBoundedNormalizer`. Has a setter method to assign values to the published signals.

Additional signals:
- `polarisation`: The calculated polarisation value
- `polarisation_ratio`: Ratio between up and down states
- Associated uncertainty measurements for polarisation values
3 changes: 2 additions & 1 deletion src/ibex_bluesky_core/callbacks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from ibex_bluesky_core.callbacks._file_logger import (
HumanReadableFileCallback,
)
from ibex_bluesky_core.callbacks._fitting import LiveFit, LiveFitLogger
from ibex_bluesky_core.callbacks._fitting import ChainedLiveFit, LiveFit, LiveFitLogger
from ibex_bluesky_core.callbacks._plotting import LivePColorMesh, LivePlot, PlotPNGSaver, show_plot
from ibex_bluesky_core.callbacks._utils import get_default_output_path
from ibex_bluesky_core.fitting import FitMethod
Expand All @@ -32,6 +32,7 @@


__all__ = [
"ChainedLiveFit",
"DocLoggingCallback",
"HumanReadableFileCallback",
"ISISCallbacks",
Expand Down
169 changes: 153 additions & 16 deletions src/ibex_bluesky_core/callbacks/_fitting.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@
import logging
import os
import warnings
from itertools import zip_longest
from pathlib import Path

import lmfit
import numpy as np
from bluesky.callbacks import CallbackBase
from bluesky.callbacks import CallbackBase, LiveFitPlot
from bluesky.callbacks import LiveFit as _DefaultLiveFit
from bluesky.callbacks.core import make_class_safe
from event_model import Event, RunStart, RunStop
from event_model import Event, EventDescriptor, RunStart, RunStop
from lmfit import Parameter
from matplotlib.axes import Axes
from numpy import typing as npt

from ibex_bluesky_core.callbacks._utils import (
DATA,
Expand All @@ -24,7 +29,7 @@

logger = logging.getLogger(__name__)

__all__ = ["LiveFit", "LiveFitLogger"]
__all__ = ["ChainedLiveFit", "LiveFit", "LiveFitLogger"]


@make_class_safe(logger=logger) # pyright: ignore (pyright doesn't understand this decorator)
Expand Down Expand Up @@ -84,20 +89,17 @@ def update_weight(self, weight: float | None = 0.0) -> None:
if self.yerr is not None:
self.weight_data.append(weight)

def update_fit(self) -> None:
"""Use the provided guess function with the most recent x and y values after every update.

Args:
None

Returns:
None

"""
def can_fit(self) -> bool:
"""Check if enough data points have been collected to fit."""
n = len(self.model.param_names)
if len(self.ydata) < n:
return len(self.ydata) >= n

def update_fit(self) -> None:
"""Use the guess function with the most recent x and y values after every update."""
if not self.can_fit():
warnings.warn(
f"LiveFitPlot cannot update fit until there are at least {n} data points",
f"""LiveFitPlot cannot update fit until there are at least
{len(self.model.param_names)} data points""",
stacklevel=1,
)
else:
Expand Down Expand Up @@ -174,7 +176,7 @@ def start(self, doc: RunStart) -> None:
self.filename = self.output_dir / f"{rb_num}" / file

def event(self, doc: Event) -> Event:
"""Start collecting, y, x and yerr data.
"""Start collecting, y, x, and yerr data.

Args:
doc: (Event): An event document.
Expand Down Expand Up @@ -251,3 +253,138 @@ def write_fields_table_uncertainty(self) -> None:

rows = zip(self.x_data, self.y_data, self.yerr_data, self.y_fit_data, strict=True)
self.csvwriter.writerows(rows)


class ChainedLiveFit(CallbackBase):
"""Processes multiple LiveFits, each fit's results inform the next, with optional plotting.

This callback handles a sequence of LiveFit instances where the parameters from each
completed fit serve as the initial guess for the subsequent fit. Optional plotting
is built in using LivePlotLib.
"""

def __init__(
self,
method: FitMethod,
y: list[str],
x: str,
*,
yerr: list[str] | None = None,
ax: list[Axes] | None = None,
) -> None:
"""Initialise ChainedLiveFit with multiple LiveFits.

Args:
method: FitMethod instance for fitting
y: List of y-axis variable names
x: x-axis variable name
yerr: Optional list of error values corresponding to y variables
ax: A list of axes to plot fits on to. Creates LiveFitPlot instances.

"""
super().__init__()

self._livefits = [
LiveFit(method=method, y=y_name, x=x, yerr=yerr_name)
for y_name, yerr_name in zip_longest(y, yerr or [])
]

self._livefitplots = [
LiveFitPlot(livefit=livefit, ax=axis)
for livefit, axis in zip(self._livefits, ax or [], strict=False)
]

def _process_doc(
self, doc: RunStart | Event | RunStop | EventDescriptor, method_name: str
) -> None:
"""Process a document for either LivePlots or LiveFits.

Args:
doc: document to process
method_name: Name of the method to call ('start', 'descriptor', 'event', or 'stop')

"""
callbacks = self._livefitplots or self._livefits
for callback in callbacks:
getattr(callback, method_name)(doc)

def start(self, doc: RunStart) -> None:
"""Process start document for all callbacks.

Args:
doc: RunStart document

"""
self._process_doc(doc, "start")

def descriptor(self, doc: EventDescriptor) -> None:
"""Process descriptor document for all callbacks.

Args:
doc: EventDescriptor document.

"""
self._process_doc(doc, "descriptor")

def event(self, doc: Event) -> Event:
"""Process event document for all callbacks.

Args:
doc: Event document

"""
init_guess = {}

for livefit in self._livefits:
rem_guess = livefit.method.guess
try:
if init_guess:
# Use previous fit results as initial guess for next fit
def guess_func(
a: npt.NDArray[np.float64], b: npt.NDArray[np.float64]
) -> dict[str, lmfit.Parameter]:
return {
name: Parameter(name, value.value)
for name, value in init_guess.items() # noqa: B023
}

# Using value.value means that paramater uncertainty
# is not carried over between fits
livefit.method.guess = guess_func

if self._livefitplots:
self._livefitplots[self._livefits.index(livefit)].event(doc)
else:
livefit.event(doc)

finally:
livefit.method.guess = rem_guess

if livefit.can_fit():
if livefit.result is None:
raise RuntimeError("LiveFit.result was None. Could not update fit.")

init_guess = livefit.result.params

from ibex_bluesky_core.callbacks import show_plot # noqa: PLC0415

show_plot()

return doc

def stop(self, doc: RunStop) -> None:
"""Process stop document and update fitting parameters.

Args:
doc: RunStop document

"""
self._process_doc(doc, "stop")

def get_livefits(self) -> list[LiveFit]:
"""Return a list of the livefits."""
return self._livefits

def get_livefitplots(self) -> list[LiveFitPlot]:
"""Return a list of the livefitplots."""
return self._livefitplots
8 changes: 7 additions & 1 deletion src/ibex_bluesky_core/devices/dae/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@
SinglePeriodSettings,
)
from ibex_bluesky_core.devices.dae._settings import DaeSettings, DaeSettingsData, DaeTimingSource
from ibex_bluesky_core.devices.dae._spectra import DaeSpectra
from ibex_bluesky_core.devices.dae._spectra import (
DaeSpectra,
PolarisedWavelengthBand,
WavelengthBand,
)
from ibex_bluesky_core.devices.dae._tcb_settings import (
DaeTCBSettings,
DaeTCBSettingsData,
Expand Down Expand Up @@ -63,13 +67,15 @@
"DaeTimingSource",
"PeriodSource",
"PeriodType",
"PolarisedWavelengthBand",
"RunstateEnum",
"SinglePeriodSettings",
"TCBCalculationMethod",
"TCBTimeUnit",
"TimeRegime",
"TimeRegimeMode",
"TimeRegimeRow",
"WavelengthBand",
]

T = TypeVar("T", bound=SignalDatatype)
Expand Down
Loading