Skip to content
Merged
Show file tree
Hide file tree
Changes from 67 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
72 changes: 65 additions & 7 deletions doc/devices/dae.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ This means that [`SimpleDae`](ibex_bluesky_core.devices.simpledae.SimpleDae) is
example running using either one DAE run per scan point, or one DAE period per scan point.

For complex use-cases, particularly those where the DAE may need to start and stop multiple
acquisitions per scan point (e.g. polarization measurements), [`SimpleDae`](ibex_bluesky_core.devices.simpledae.SimpleDae) is unlikely to be
acquisitions per scan point (e.g. Polarisation measurements), [`SimpleDae`](ibex_bluesky_core.devices.simpledae.SimpleDae) is unlikely to be
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with this change.

image

suitable; instead the [`Dae`](ibex_bluesky_core.devices.dae.Dae) class should be subclassed directly to allow for finer control.

## Example configurations
Expand Down Expand Up @@ -295,10 +295,10 @@ 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
### Polarisation/Asymmetry

ibex_bluesky_core provides a helper method,
{py:obj}`ibex_bluesky_core.devices.simpledae.polarization`, for calculating the quantity (a-b)/(a+b). This quantity is used, for example, in neutron polarization measurements, and in calculating asymmetry for muon measurements.
{py:obj}`ibex_bluesky_core.utils.polarisation`, for calculating the quantity (a-b)/(a+b). This quantity is used, for example, in neutron polarisation measurements, and in calculating asymmetry for muon measurements.

For this expression, scipp's default uncertainty propagation rules cannot be used as the uncertainties on (a-b) are correlated with those of (a+b) in the division step - but scipp assumes uncorrelated data. This helper method calculates the uncertainties following linear error propagation theory, using the partial derivatives of the above expression.

Expand All @@ -314,7 +314,7 @@ Which then means the variances computed by this helper function are:
$ Variance = (\frac{\delta}{\delta a}^2 * variance_a) + (\frac{\delta}{\delta b}^2 * variance_b) $


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

Instrument-Specific Interpretations
Expand All @@ -328,6 +328,10 @@ Similar to LARMOR, A and B represent intensities before and after flipper switch
Muon Instruments
A and B refer to Measurements from different detector banks.

{py:obj}`ibex_bluesky_core.utils.polarisation`

See [`PolarisationReducer`](#PolarisationReducer) 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 @@ -380,6 +384,63 @@ 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.

### DualRunDae

[`DualRunDae`](ibex_bluesky_core.devices.polarisingdae.DualRunDae) 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 three separate reduction strategies
- Up & Down Reducers, which run after each run
- Main reducer, which runs after everything else

### polarising_dae

[`polarising_dae`](ibex_bluesky_core.devices.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

#### MultiWavelengthBandNormalizer

[`MultiWavelengthBandNormalizer`](ibex_bluesky_core.devices.polarisingdae.MultiWavelengthBandNormalizer) 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

[`PolarisationReducer`](ibex_bluesky_core.devices.polarisingdae.PolarisationReducer) calculates polarisation from 'spin-up' and 'spin-down' states of a polarising DAE. Uses the [`Polarisation`](#polarisationasymmetry) 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 @@ -498,9 +559,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 Down
62 changes: 56 additions & 6 deletions doc/fitting/fitting.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Fitting Callback
# Fitting Callbacks

Similar to [`LivePlot`](../callbacks/plotting.md), [`ibex_bluesky_core`](ibex_bluesky_core) provides a thin wrapper around Bluesky's [`LiveFit`](ibex_bluesky_core.callbacks.LiveFit) class, enhancing it with additional functionality to better support real-time data fitting. This wrapper not only offers a wide selection of models to fit your data on, but also introduces guess generation for fit parameters. As new data points are acquired, the wrapper refines these guesses dynamically, improving the accuracy of the fit with each additional piece of data, allowing for more efficient and adaptive real-time fitting workflows.

Expand All @@ -7,7 +7,8 @@ In order to use the wrapper, import[`LiveFit`](ibex_bluesky_core.callbacks.LiveF
```py
from ibex_bluesky_core.callbacks.fitting import LiveFit
```
**Note:** that you do not *need* [`LivePlot`](ibex_bluesky_core.callbacks.LivePlot) for [`LiveFit`](ibex_bluesky_core.callbacks.LiveFit) to work but it may be useful to know visaully how well the model fits to the raw data.
.. note::
that you do not *need* [`LivePlot`](ibex_bluesky_core.callbacks.LivePlot) for [`LiveFit`](ibex_bluesky_core.callbacks.LiveFit) to work but it may be useful to know visaully how well the model fits to the raw data.

## Configuration

Expand All @@ -31,7 +32,8 @@ fit_callback = LiveFit(Gaussian.fit(), y="y_signal", x="x_signal", yerr="yerr_si
fit_plot_callback = LiveFitPlot(fit_callback, ax=ax, color="r")
```

**Note:** that the [`LiveFit`](ibex_bluesky_core.callbacks.LiveFit) callback doesn't directly do the plotting, it will return function parameters of the model its trying to fit to; a [`LiveFit`](ibex_bluesky_core.callbacks.LiveFit) object must be passed to `LiveFitPlot` which can then be subscribed to the `RunEngine`. See the [Bluesky Documentation](https://blueskyproject.io/bluesky/main/callbacks.html#livefitplot) for information on the various arguments that can be passed to the `LiveFitPlot` class.
.. note::
that the [`LiveFit`](ibex_bluesky_core.callbacks.LiveFit) callback doesn't directly do the plotting, it will return function parameters of the model its trying to fit to; a [`LiveFit`](ibex_bluesky_core.callbacks.LiveFit) object must be passed to `LiveFitPlot` which can then be subscribed to the `RunEngine`. See the [Bluesky Documentation](https://blueskyproject.io/bluesky/main/callbacks.html#livefitplot) for information on the various arguments that can be passed to the `LiveFitPlot` class.

Using the `yerr` argument allows you to pass uncertainties via a signal to LiveFit, so that the "weight" of each point influences the fit produced. By not providing a signal name you choose not to use uncertainties/weighting in the fitting calculation. Each weight is computed as `1/(standard deviation at point)` and is taken into account to determine how much a point affects the overall fit of the data. Same as the rest of [`LiveFit`](ibex_bluesky_core.callbacks.LiveFit), the fit will be updated after every new point collected now taking into account the weights of each point. Uncertainty data is collected from Bluesky event documents after each new point.

Expand Down Expand Up @@ -89,7 +91,8 @@ lf = LiveFit([FIT].fit(), y="y_signal", x="x_signal", update_every=0.5)

The `[FIT].fit()` function will pass the [`FitMethod`](ibex_bluesky_core.fitting.FitMethod) object straight to the [`LiveFit`](ibex_bluesky_core.callbacks.LiveFit) class.

**Note:** that for the fits in the above table that require parameters, you will need to pass value(s) to their `.fit` method. For example Polynomial fitting:
.. note::
that for the fits in the above table that require parameters, you will need to pass value(s) to their `.fit` method. For example Polynomial fitting:

```py
lf = LiveFit(Polynomial.fit(3), y="y_signal", x="x_signal", update_every=0.5)
Expand Down Expand Up @@ -146,7 +149,8 @@ lf = LiveFit(fit_method, y="y_signal", x="x_signal", update_every=0.5)
# Then subscribe to LiveFitPlot(lf, ...)
```

**Note:** that the parameters returned from the guess function must allocate to the arguments to the model function, ignoring the independant variable e.g `x` in this case. Array-like structures are not allowed. See the [lmfit documentation](https://lmfit.github.io/lmfit-py/parameters.html) for more information.
.. note::
that the parameters returned from the guess function must allocate to the arguments to the model function, ignoring the independant variable e.g `x` in this case. Array-like structures are not allowed. See the [lmfit documentation](https://lmfit.github.io/lmfit-py/parameters.html) for more information.

#### Option 2: Continued

Expand Down Expand Up @@ -201,10 +205,56 @@ lf = LiveFit(fit_method, y="y_signal", x="x_signal", update_every=0.5)

Or you can create a completely user-defined fitting method.

**Note:** that for fits that require arguments, you will need to pass values to their respecitive `.model` and `.guess` functions. E.g for `Polynomial` fitting:
.. note::
that for fits that require arguments, you will need to pass values to their respecitive `.model` and `.guess` functions. E.g for `Polynomial` fitting:

```py
fit_method = FitMethod(Polynomial.model(3), different_guess) # If using a custom guess function
lf = LiveFit(fit_method, ...)
```
See the [standard fits](#models) list above for standard fits which require parameters. It gets more complicated if you want to define your own custom model or guess which you want to pass parameters to. You will have to define a function that takes these parameters and returns the model / guess function with the subsituted values.

## Chained Fitting

[`ChainedLiveFit`](ibex_bluesky_core.callbacks.ChainedLiveFit) is a specialised callback that manages multiple LiveFit instances in a chain, where each fit's results inform the next fit's initial parameters. This is particularly useful when dealing with complex data sets where subsequent fits depend on the parameters obtained from previous fits.

This is useful for when you need to be careful with your curve fitting due to the presence of noisy data. It allows you to fit your widest (full) wavelength band first and then using its fit parameters as the initial guess of the parameters for the next fit

# Usage
To show how we expect this to be used we will use the PolarisingDae and wavelength bands to highlight the need for the carry over of fitting parameters. Below shows two wavelength bands, first bigger than the second, we will fit to the data in the first and carry it over to the data in the second to improve its, otherwise worse, fit.

```python
# Needed for PolarisingDae
flipper = block_rw(float, "flipper")
total_flight_path_length = sc.scalar(value=10, unit=sc.units.m)

x_axis = block_rw(float, "x_axis", write_config=BlockWriteConfig(settle_time_s=0.5))
wavelength_band_0 = sc.array(dims=["tof"], values=[0, 9999999999.0], unit=sc.units.angstrom, dtype="float64")
wavelength_band_1 = sc.array(dims=["tof"], values=[0.0, 0.07], unit=sc.units.angstrom, dtype="float64")

dae = polarising_dae(det_pixels=[1], frames=50, flipper=flipper, flipper_states=(0.0, 1.0), intervals=[wavelength_band_0, wavelength_band_1], total_flight_path_length=total_flight_path_length, monitor=2)

def plan() -> Generator[Msg, None, None]:

fig, (ax1, ax2) = yield from call_qt_aware(plt.subplots, 2)
chained_fit = ChainedLiveFit(method=Linear.fit(), y=[dae.reducer.wavelength_bands[0].polarisation.name, dae.reducer.wavelength_bands[1].polarisation.name], x=bob.name, ax=[ax1,ax2])

# Subscribe chained_fit to RE and run do a scan for example
# chained_fit.get_livefits()[-1].result will give you the fitting results for the last wavelength band
```

- You are expected to pass in the list of signal names for each independent variable to `y` in order of how you want the subsequent fitting to go.
- You may also pass in a list of matplotlib axes, which will mean that LiveFitPlots are created per LiveFit, and it will plot the each respective fit to an axis. LiveFitPlots are not created if you do not pass `ax`.
- Similar to the `y` parameter, you may pass signal names which correspond to uncertainty values for each independent variable.

```{hint}
The method for fitting is the same across all independent variables.
```

```{note}
Parameter uncertainties are not carried over between fits
```

```{important}
If a fit fails to converge, subsequent fits will use their default guess functions
```
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
Loading