Skip to content

Commit

Permalink
Merge pull request #64 from ISISComputingGroup/38_generate_files_cont…
Browse files Browse the repository at this point in the history
…aining_fit_results

38 generate files containing fit results
  • Loading branch information
Tom-Willemsen authored Dec 9, 2024
2 parents e48d253 + bafb31b commit 55cdad8
Show file tree
Hide file tree
Showing 15 changed files with 382 additions and 20 deletions.
Binary file added doc/_static/images_fits/damped_osc.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
35 changes: 35 additions & 0 deletions doc/fitting/livefit_logger.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Fitting Files Callback
## Fitting Files

The callback ({py:obj}`ibex_bluesky_core.callbacks.fitting.livefit_logger.LiveFitLogger`) exists to write all fitting metrics from `LiveFit` to file. These are designed to be human readable files rather than machine readable.

This callback provides you with useful metrics such as `R-squared` and `chi-square`, then providing you with a table of the raw collected data included modelled `y` data and `y` uncertainty.

### Example
An example of using this could be:

```{code} python
def some_plan() -> Generator[Msg, None, None]:
... # Set up prefix, reducers, controllers etc. here
@subs_decorator(
[
LiveFitLogger(
lf, # LiveFit
y=reducer.intensity.name,
x=block.name,
output_dir=Path(f"C:\\Instrument\\Var\\logs\\bluesky\\fitting"),
postfix="bob" # Make sure to have different postfixes if using
# more than 1 LiveFitLogger per run
yerr=reducer.intensity_stddev.name, # Not required
)
... # Other callbacks ie. live table/plot here - you can use multiple!
]
)
def _inner() -> Generator[Msg, None, None]:
... # Continue to plan
```

This will put the all fitting data collected over the run into a `.csv` file, named after the `uid` of the scan, in the `C:\\Instrument\\Var\\logs\\bluesky\\fitting` path provided to the callback. You should provide a `postfix` to append to the end of the filename to disambiguate different fits and to avoid overwriting fitting files- it is only one file per fit completed.

If you provide a signal name for the `yerr` argument then an extra column for `y uncertainty` will be displayed in the fitting file. You have the option to not provide anything for this argument if you do not want to have uncertainty information in your fitting file. Keep in mind that even if you provide `yerr` in `LiveFitLogger`, you will still need to provide `yerr` in `LiveFit` if you want uncertainty/weight per point to influence the fit.
16 changes: 8 additions & 8 deletions doc/fitting/standard_fits.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ y = c_{n}x^n + c_{n-1}x^n-1 + ... + c_1 * x^1 + c_0
y = \text{amp} * e^{-\frac{(x - x0) ^ 2}{2 * \text{sigma}^2}} + \text{background}
```

![GaussianModel](./images_fits/gaussian.png)
![GaussianModel](../_static/images_fits/gaussian.png)

## Lorentzian

Expand All @@ -42,7 +42,7 @@ y = \text{amp} * e^{-\frac{(x - x0) ^ 2}{2 * \text{sigma}^2}} + \text{background
y = \frac{\text{amp}}{1 + \frac{x - \text{center}}{\text{sigma}}^2} + \text{background}
```

![LorentzianModel](./images_fits/lorentzian.png)
![LorentzianModel](../_static/images_fits/lorentzian.png)

## Damped Oscillator (DampedOsc)

Expand All @@ -55,7 +55,7 @@ y = \frac{\text{amp}}{1 + \frac{x - \text{center}}{\text{sigma}}^2} + \text{back
y = \text{amp} * \cos((x - \text{center}) * \text{freq}) * e^{-\frac{x - \text{center}}{\text{width}^ 2}}
```

![DampedOscModel](./images_fits/dampedosc.png)
![DampedOscModel](../_static/images_fits/damped_osc.png)

## Slit Scan (SlitScan)

Expand All @@ -77,7 +77,7 @@ y = \text{amp} * \cos((x - \text{center}) * \text{freq}) * e^{-\frac{x - \text{c
y = \min(\text{lin_seg}, \text{exp_seg})
```

![SlitScanModel](./images_fits/slitscan.png)
![SlitScanModel](../_static/images_fits/slit_scan.png)

## Error Function (ERF)

Expand All @@ -90,7 +90,7 @@ y = \min(\text{lin_seg}, \text{exp_seg})
y = background + scale * erf(stretch * (x - cen))
```

![ERFModel](./images_fits/erf.png)
![ERFModel](../_static/images_fits/erf.png)

## Complementary Error Function (ERFC)

Expand All @@ -103,7 +103,7 @@ y = background + scale * erf(stretch * (x - cen))
y = background + scale * erfc(stretch * (x - cen))
```

![ERFCModel](./images_fits/erfc.png)
![ERFCModel](../_static/images_fits/erfc.png)

## Top Hat (TopHat)

Expand All @@ -120,7 +120,7 @@ y =
\end{cases}
```

![TopHatModel](./images_fits/tophat.png)
![TopHatModel](../_static/images_fits/tophat.png)

## Trapezoid

Expand All @@ -140,4 +140,4 @@ g(x) = \max(f(x), \text{background})
y = \min(g(x), \text{background} + \text{height})
```

![TrapezoidModel](./images_fits/trapezoid.png)
![TrapezoidModel](../_static/images_fits/trapezoid.png)
10 changes: 10 additions & 0 deletions manual_system_tests/dae_scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from ibex_bluesky_core.callbacks.file_logger import HumanReadableFileCallback
from ibex_bluesky_core.callbacks.fitting import LiveFit
from ibex_bluesky_core.callbacks.fitting.fitting_utils import Linear
from ibex_bluesky_core.callbacks.fitting.livefit_logger import LiveFitLogger
from ibex_bluesky_core.callbacks.plotting import LivePlot
from ibex_bluesky_core.devices import get_pv_prefix
from ibex_bluesky_core.devices.block import block_rw_rbv
Expand Down Expand Up @@ -112,12 +113,21 @@ def dae_scan_plan() -> Generator[Msg, None, None]:
dae.good_frames.name,
]
),
LiveFitLogger(
lf,
y=reducer.intensity.name,
x=block.name,
output_dir=Path("C:\\Instrument\\Var\\logs\\bluesky\\fitting"),
yerr=reducer.intensity_stddev.name,
postfix="manual_test",
),
]
)
def _inner() -> Generator[Msg, None, None]:
yield from bps.mv(dae.number_of_periods, NUM_POINTS) # type: ignore
# Pyright does not understand as bluesky isn't typed yet
yield from bp.scan([dae], block, 0, 10, num=NUM_POINTS)
print(lf.result.fit_report())

yield from _inner()

Expand Down
6 changes: 3 additions & 3 deletions src/ibex_bluesky_core/callbacks/fitting/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,17 @@ def __init__(
guess (Callable): The guess function to use.
"""
self.guess = guess

if callable(model):
self.model = lmfit.Model(model)
else:
self.model = model

self.guess = guess


@make_class_safe(logger=logger) # pyright: ignore (pyright doesn't understand this decorator)
class LiveFit(_DefaultLiveFit):
"""Live fit, customized for IBEX."""
"""LiveFit, customized for IBEX."""

def __init__(
self, method: FitMethod, y: str, x: str, *, update_every: int = 1, yerr: str | None = None
Expand Down
44 changes: 35 additions & 9 deletions src/ibex_bluesky_core/callbacks/fitting/fitting_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
class Fit(ABC):
"""Base class for all fits."""

equation: str = ""

@classmethod
@abstractmethod
def model(cls, *args: int) -> lmfit.Model:
Expand Down Expand Up @@ -56,6 +58,8 @@ def fit(cls, *args: int) -> FitMethod:
class Gaussian(Fit):
"""Gaussian Fitting."""

equation = "amp * exp(-((x - x0) ** 2) / (2 * sigma**2)) + background"

@classmethod
def model(cls, *args: int) -> lmfit.Model:
"""Gaussian Model."""
Expand All @@ -68,7 +72,7 @@ def model(

return amp * np.exp(-((x - x0) ** 2) / (2 * sigma**2)) + background

return lmfit.Model(model)
return lmfit.Model(model, name=f"{cls.__name__} [{cls.equation}]")

@classmethod
def guess(
Expand Down Expand Up @@ -103,6 +107,8 @@ def guess(
class Lorentzian(Fit):
"""Lorentzian Fitting."""

equation = "amp / (1 + ((x - center) / sigma) ** 2) + background"

@classmethod
def model(cls, *args: int) -> lmfit.Model:
"""Lorentzian Model."""
Expand All @@ -115,7 +121,7 @@ def model(

return amp / (1 + ((x - center) / sigma) ** 2) + background

return lmfit.Model(model)
return lmfit.Model(model, name=f"{cls.__name__} [{cls.equation}]")

@classmethod
def guess(
Expand Down Expand Up @@ -173,14 +179,16 @@ def guess(
class Linear(Fit):
"""Linear Fitting."""

equation = "c1 * x + c0"

@classmethod
def model(cls, *args: int) -> lmfit.Model:
"""Linear Model."""

def model(x: npt.NDArray[np.float64], c1: float, c0: float) -> npt.NDArray[np.float64]:
return c1 * x + c0

return lmfit.Model(model)
return lmfit.Model(model, name=f"{cls.__name__} [{cls.equation}]")

@classmethod
def guess(
Expand All @@ -193,6 +201,8 @@ def guess(
class Polynomial(Fit):
"""Polynomial Fitting."""

equation = "cn * x^n + ... + c1 * x^1 + c0"

@classmethod
def _check_degree(cls, args: tuple[int, ...]) -> int:
"""Check that polynomial degree is valid."""
Expand Down Expand Up @@ -233,6 +243,8 @@ def guess(
class DampedOsc(Fit):
"""Damped Oscillator Fitting."""

equation = "amp * cos((x - center) * freq) * exp(-(((x - center) / width) ** 2))"

@classmethod
def model(cls, *args: int) -> lmfit.Model:
"""Damped Oscillator Model."""
Expand All @@ -242,7 +254,7 @@ def model(
) -> npt.NDArray[np.float64]:
return amp * np.cos((x - center) * freq) * np.exp(-(((x - center) / width) ** 2))

return lmfit.Model(model)
return lmfit.Model(model, name=f"{cls.__name__} [{cls.equation}]")

@classmethod
def guess(
Expand Down Expand Up @@ -271,6 +283,10 @@ def guess(
class SlitScan(Fit):
"""Slit Scan Fitting."""

equation = """See
https://isiscomputinggroup.github.io/ibex_bluesky_core/fitting/standard_fits.html#slit-scan-slitscan
for model function"""

@classmethod
def model(cls, *args: int) -> lmfit.Model:
"""Slit Scan Model."""
Expand Down Expand Up @@ -306,7 +322,7 @@ def model(

return y

return lmfit.Model(model)
return lmfit.Model(model, name=f"{cls.__name__} [{cls.equation}]")

@classmethod
def guess(
Expand Down Expand Up @@ -343,6 +359,8 @@ def guess(
class ERF(Fit):
"""Error Function Fitting."""

equation = "background + scale * erf(stretch * (x - cen))"

@classmethod
def model(cls, *args: int) -> lmfit.Model:
"""Error Function Model."""
Expand All @@ -352,7 +370,7 @@ def model(
) -> npt.NDArray[np.float64]:
return background + scale * scipy.special.erf(stretch * (x - cen))

return lmfit.Model(model)
return lmfit.Model(model, name=f"{cls.__name__} [{cls.equation}]")

@classmethod
def guess(
Expand All @@ -378,6 +396,8 @@ def guess(
class ERFC(Fit):
"""Complementary Error Function Fitting."""

equation = "background + scale * erfc(stretch * (x - cen))"

@classmethod
def model(cls, *args: int) -> lmfit.Model:
"""Complementary Error Function Model."""
Expand All @@ -387,7 +407,7 @@ def model(
) -> npt.NDArray[np.float64]:
return background + scale * scipy.special.erfc(stretch * (x - cen))

return lmfit.Model(model)
return lmfit.Model(model, name=f"{cls.__name__} [{cls.equation}]")

@classmethod
def guess(
Expand All @@ -413,6 +433,8 @@ def guess(
class TopHat(Fit):
"""Top Hat Fitting."""

equation = "if (abs(x - cen) < width / 2) { background + height } else { background }"

@classmethod
def model(cls, *args: int) -> lmfit.Model:
"""Top Hat Model."""
Expand All @@ -424,7 +446,7 @@ def model(
y[np.abs(x - cen) < width / 2] = height
return background + y

return lmfit.Model(model)
return lmfit.Model(model, name=f"{cls.__name__} [{cls.equation}]")

@classmethod
def guess(
Expand Down Expand Up @@ -458,6 +480,10 @@ def guess(
class Trapezoid(Fit):
"""Trapezoid Fitting."""

equation = """
y = clip(y_offset + height + background - gradient * abs(x - cen),
background, background + height)"""

@classmethod
def model(cls, *args: int) -> lmfit.Model:
"""Trapezoid Model."""
Expand All @@ -475,7 +501,7 @@ def model(
y = np.minimum(y, background + height)
return y

return lmfit.Model(model)
return lmfit.Model(model, name=f"{cls.__name__} [{cls.equation}]")

@classmethod
def guess(
Expand Down
Loading

0 comments on commit 55cdad8

Please sign in to comment.