Skip to content

Commit

Permalink
Merge branch 'main' into can_we_use_3_12_yet
Browse files Browse the repository at this point in the history
  • Loading branch information
iangillingham-stfc authored Nov 26, 2024
2 parents 4f5f6fd + 4eacc3c commit 4fb04b2
Show file tree
Hide file tree
Showing 17 changed files with 59 additions and 99 deletions.
2 changes: 1 addition & 1 deletion doc/fitting/fitting.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ We support **standard fits** for the following trends in data. See [Standard Fit
| Gaussian | [Gaussian](./standard_fits.md#gaussian) | None |
| Lorentzian | [Lorentzian](./standard_fits.md#lorentzian) | None |
| Damped Oscillator | [DampedOsc](./standard_fits.md#damped-oscillator-dampedosc) | None |
| Slit Scan Fit | [SlitScan](./standard_fits.md#slit-scan-slitscan) | Max Slit Size (int) |
| Slit Scan Fit | [SlitScan](./standard_fits.md#slit-scan-slitscan) | None |
| Error Function | [ERF](./standard_fits.md#error-function-erf) | None |
| Complementary Error Function | [ERFC](./standard_fits.md/#complementary-error-function-erfc) | None |
| Top Hat | [TopHat](./standard_fits.md#top-hat-tophat) | None |
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ classifiers = [

dependencies = [
"bluesky",
"ophyd-async[ca]",
"ophyd-async[ca] >= 0.8.0",
"matplotlib",
"lmfit",
"scipy",
Expand Down
36 changes: 6 additions & 30 deletions src/ibex_bluesky_core/callbacks/fitting/fitting_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,14 +272,6 @@ def guess(
class SlitScan(Fit):
"""Slit Scan Fitting."""

@classmethod
def _check_input(cls, args: tuple[int, ...]) -> int:
"""Check that provided maximum slit size is atleast 0."""
max_slit_gap = args[0] if args else 1
if not (0 <= max_slit_gap):
raise ValueError("The slit gap should be atleast 0.")
return max_slit_gap

@classmethod
def model(cls, *args: int) -> lmfit.Model:
"""Slit Scan Model."""
Expand Down Expand Up @@ -326,34 +318,18 @@ def guess(
def guess(
x: npt.NDArray[np.float64], y: npt.NDArray[np.float64]
) -> dict[str, lmfit.Parameter]:
max_slit_size = cls._check_input(args)

# Guessing. gradient of linear-slope part of function
dy = np.gradient(y) # Return array of differences in y
max_dy = np.max(dy) # Return max y difference, this will always be on the upwards slope
dx = abs(x[1] - x[0]) # Find x step
gradient = max_dy / dx

d2y = np.diff(dy) # Double differentiate y to find how gradients change
inflection0 = x[np.argmax(d2y)] # Where there is positive gradient change

background = min(y) # The lowest y value is the background
if gradient != 0.0:
inflections_diff = -(background - y[np.argmax(y)]) / gradient
else:
inflections_diff = dx # Fallback case, guess one x step
# As linear, using y - y1 = m(x - x1) -> x = (y - y1) / gradient - x1

# The highest y value + slightly more to account for further convergence
# - y distance travelled from inflection0 to inflection1
height_above_inflection1 = np.max(y) + (y[-1] - y[-2]) - (gradient * inflections_diff)
background = np.min(y)
inflection0 = np.min(x) + (1 / 3) * (np.max(x) - np.min(x))
inflections_diff = (1 / 3) * (np.max(x) - np.min(x))
gradient = 2 * (np.max(y) - np.min(y)) / (np.max(x) - np.min(x))
height_above_inflection1 = (np.max(y) - np.min(y)) / 5.0

init_guess = {
"background": lmfit.Parameter("background", background),
"inflection0": lmfit.Parameter("inflection0", inflection0),
"gradient": lmfit.Parameter("gradient", gradient, min=0),
"inflections_diff": lmfit.Parameter(
"inflections_diff", inflections_diff, min=max_slit_size
"inflections_diff", inflections_diff, min=0, max=float(np.max(x) - np.min(x))
),
"height_above_inflection1": lmfit.Parameter(
"height_above_inflection1", height_above_inflection1, min=0
Expand Down
8 changes: 5 additions & 3 deletions src/ibex_bluesky_core/devices/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
import binascii
import os
import zlib
from typing import Type
from typing import Type, TypeVar

from ophyd_async.core import SignalRW, T
from ophyd_async.epics.signal import epics_signal_rw
from ophyd_async.core import SignalDatatype, SignalRW
from ophyd_async.epics.core import epics_signal_rw

T = TypeVar("T", bound=SignalDatatype)


def get_pv_prefix() -> str:
Expand Down
11 changes: 6 additions & 5 deletions src/ibex_bluesky_core/devices/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,22 @@
from bluesky.protocols import Locatable, Location, Movable, Triggerable
from ophyd_async.core import (
AsyncStatus,
HintedSignal,
SignalDatatype,
SignalR,
SignalRW,
StandardReadable,
StandardReadableFormat,
observe_value,
)
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
from ophyd_async.epics.motor import Motor
from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw

from ibex_bluesky_core.devices import get_pv_prefix

logger = logging.getLogger(__name__)

# Block data type
T = TypeVar("T")
T = TypeVar("T", bound=SignalDatatype)


__all__ = [
Expand Down Expand Up @@ -98,7 +99,7 @@ def __init__(self, prefix: str, name: str = "") -> None:
name: ophyd device name
"""
with self.add_children_as_readables(HintedSignal):
with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
# When explicitly reading run control, the most obvious signal that people will be
# interested in is whether the block is in range or not.
self.in_range = epics_signal_r(bool, f"{prefix}INRANGE")
Expand Down Expand Up @@ -127,7 +128,7 @@ def __init__(self, datatype: Type[T], prefix: str, block_name: str) -> None:
block_name: the name of the block
"""
with self.add_children_as_readables(HintedSignal):
with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
self.readback: SignalR[T] = epics_signal_r(datatype, f"{prefix}CS:SB:{block_name}")

# Run control doesn't need to be read by default
Expand Down
17 changes: 7 additions & 10 deletions src/ibex_bluesky_core/devices/dae/dae.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
"""ophyd-async devices for communicating with the ISIS data acquisition electronics."""

from enum import Enum

from numpy import int32
from numpy.typing import NDArray
from ophyd_async.core import SignalR, SignalRW, StandardReadable
from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw
from ophyd_async.core import Array1D, SignalR, SignalRW, StandardReadable, StrictEnum
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw

from ibex_bluesky_core.devices import isis_epics_signal_rw
from ibex_bluesky_core.devices.dae.dae_controls import DaeControls
Expand All @@ -18,7 +15,7 @@
from ibex_bluesky_core.devices.dae.dae_tcb_settings import DaeTCBSettings


class RunstateEnum(str, Enum):
class RunstateEnum(StrictEnum):
"""The run state."""

PROCESSING = "PROCESSING"
Expand Down Expand Up @@ -82,11 +79,11 @@ def __init__(self, prefix: str, name: str = "DAE") -> None:
self.period_settings = DaePeriodSettings(dae_prefix)
self.tcb_settings = DaeTCBSettings(dae_prefix)

self.raw_spectra_integrals: SignalR[NDArray[int32]] = epics_signal_r(
NDArray[int32], f"{dae_prefix}SPECINTEGRALS"
self.raw_spectra_integrals: SignalR[Array1D[int32]] = epics_signal_r(
Array1D[int32], f"{dae_prefix}SPECINTEGRALS"
)
self.raw_spectra_data: SignalR[NDArray[int32]] = epics_signal_r(
NDArray[int32], f"{dae_prefix}SPECDATA"
self.raw_spectra_data: SignalR[Array1D[int32]] = epics_signal_r(
Array1D[int32], f"{dae_prefix}SPECDATA"
)

self.monitor = DaeMonitor(dae_prefix)
Expand Down
2 changes: 1 addition & 1 deletion src/ibex_bluesky_core/devices/dae/dae_controls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from bluesky.protocols import Movable
from ophyd_async.core import AsyncStatus, SignalW, SignalX, StandardReadable
from ophyd_async.epics.signal import epics_signal_w, epics_signal_x
from ophyd_async.epics.core import epics_signal_w, epics_signal_x

logger = logging.getLogger(__name__)

Expand Down
2 changes: 1 addition & 1 deletion src/ibex_bluesky_core/devices/dae/dae_event_mode.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""ophyd-async devices and utilities for the DAE event mode statistics."""

from ophyd_async.core import SignalR, StandardReadable
from ophyd_async.epics.signal import epics_signal_r
from ophyd_async.epics.core import epics_signal_r


class DaeEventMode(StandardReadable):
Expand Down
2 changes: 1 addition & 1 deletion src/ibex_bluesky_core/devices/dae/dae_monitor.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""ophyd-async devices and utilities for a DAE monitor."""

from ophyd_async.core import SignalR, StandardReadable
from ophyd_async.epics.signal import epics_signal_r
from ophyd_async.epics.core import epics_signal_r


class DaeMonitor(StandardReadable):
Expand Down
2 changes: 1 addition & 1 deletion src/ibex_bluesky_core/devices/dae/dae_period.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""ophyd-async devices and utilities for a DAE period."""

from ophyd_async.core import SignalR, StandardReadable
from ophyd_async.epics.signal import epics_signal_r
from ophyd_async.epics.core import epics_signal_r


class DaePeriod(StandardReadable):
Expand Down
4 changes: 2 additions & 2 deletions src/ibex_bluesky_core/devices/dae/dae_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from xml.etree.ElementTree import tostring

from bluesky.protocols import Locatable, Location, Movable
from ophyd_async.core import AsyncStatus, Device, SignalRW
from ophyd_async.core import AsyncStatus, SignalRW, StandardReadable

from ibex_bluesky_core.devices import (
isis_epics_signal_rw,
Expand Down Expand Up @@ -150,7 +150,7 @@ def _convert_dae_settings_to_xml(current_xml: str, settings: DaeSettingsData) ->
return tostring(root, encoding="unicode")


class DaeSettings(Device, Locatable, Movable):
class DaeSettings(StandardReadable, Locatable, Movable):
"""Subdevice for the DAE general settings."""

def __init__(self, dae_prefix: str, name: str = "") -> None:
Expand Down
20 changes: 10 additions & 10 deletions src/ibex_bluesky_core/devices/dae/dae_spectra.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
from event_model.documents.event_descriptor import DataKey
from numpy import float32
from numpy.typing import NDArray
from ophyd_async.core import SignalR, StandardReadable
from ophyd_async.epics.signal import epics_signal_r
from ophyd_async.core import Array1D, SignalR, StandardReadable
from ophyd_async.epics.core import epics_signal_r

VARIANCE_ADDITION = 0.5
logger = logging.getLogger(__name__)
Expand All @@ -21,17 +21,17 @@ def __init__(self, dae_prefix: str, *, spectra: int, period: int, name: str = ""
"""Set up signals for a single DAE spectra."""
# x-axis; time-of-flight.
# These are bin-centre coordinates.
self.tof: SignalR[NDArray[float32]] = epics_signal_r(
NDArray[float32], f"{dae_prefix}SPEC:{period}:{spectra}:X"
self.tof: SignalR[Array1D[float32]] = epics_signal_r(
Array1D[float32], f"{dae_prefix}SPEC:{period}:{spectra}:X"
)
self.tof_size: SignalR[int] = epics_signal_r(
int, f"{dae_prefix}SPEC:{period}:{spectra}:X.NORD"
)

# x-axis; time-of-flight.
# These are bin-edge coordinates, with a size one more than the corresponding data.
self.tof_edges: SignalR[NDArray[float32]] = epics_signal_r(
NDArray[float32], f"{dae_prefix}SPEC:{period}:{spectra}:XE"
self.tof_edges: SignalR[Array1D[float32]] = epics_signal_r(
Array1D[float32], f"{dae_prefix}SPEC:{period}:{spectra}:XE"
)
self.tof_edges_size: SignalR[int] = epics_signal_r(
int, f"{dae_prefix}SPEC:{period}:{spectra}:XE.NORD"
Expand All @@ -42,8 +42,8 @@ def __init__(self, dae_prefix: str, *, spectra: int, period: int, name: str = ""
# that ToF bin.
# - Unsuitable for summing counts directly.
# - Will give a continuous plot for non-uniform bin sizes.
self.counts_per_time: SignalR[NDArray[float32]] = epics_signal_r(
NDArray[float32], f"{dae_prefix}SPEC:{period}:{spectra}:Y"
self.counts_per_time: SignalR[Array1D[float32]] = epics_signal_r(
Array1D[float32], f"{dae_prefix}SPEC:{period}:{spectra}:Y"
)
self.counts_per_time_size: SignalR[int] = epics_signal_r(
int, f"{dae_prefix}SPEC:{period}:{spectra}:Y.NORD"
Expand All @@ -53,8 +53,8 @@ def __init__(self, dae_prefix: str, *, spectra: int, period: int, name: str = ""
# This is unnormalized number of counts per ToF bin.
# - Suitable for summing counts
# - This will give a discontinuous plot for non-uniform bin sizes.
self.counts: SignalR[NDArray[float32]] = epics_signal_r(
NDArray[float32], f"{dae_prefix}SPEC:{period}:{spectra}:YC"
self.counts: SignalR[Array1D[float32]] = epics_signal_r(
Array1D[float32], f"{dae_prefix}SPEC:{period}:{spectra}:YC"
)
self.counts_size: SignalR[int] = epics_signal_r(
int, f"{dae_prefix}SPEC:{period}:{spectra}:YC.NORD"
Expand Down
2 changes: 1 addition & 1 deletion src/ibex_bluesky_core/preprocessors.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from bluesky import preprocessors as bpp
from bluesky.utils import Msg, single_gen
from ophyd_async.core import SignalR
from ophyd_async.epics.signal import epics_signal_r
from ophyd_async.epics.core import epics_signal_r
from ophyd_async.plan_stubs import ensure_connected

from ibex_bluesky_core.devices import get_pv_prefix
Expand Down
16 changes: 4 additions & 12 deletions tests/callbacks/fitting/test_fitting_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,14 +364,6 @@ def test_guess_freq(self):


class TestSlitScan:
def test_max_slit_width(self):
x = np.arange(-5.0, 5.0, 1.0)
y = np.zeros(10)

# -1 is not a valid max slit gap
with pytest.raises(ValueError):
SlitScan.guess(-1)(x, y)

class TestSlitScanModel:
def test_slit_scan_model(self):
x = np.arange(-5.0, 5.0, 1.0)
Expand Down Expand Up @@ -438,28 +430,28 @@ def test_guess_gradient(self):
y = np.array([1.0, 1.0, 2.0, 3.0, 4.0, 4.0])
outp = SlitScan.guess()(x, y)

assert 1.0 == pytest.approx(outp["gradient"].value, rel=1e-2)
assert 1.2 == pytest.approx(outp["gradient"].value, rel=1e-2)

def test_guess_inflections_diff(self):
x = np.array([-1.0, 0.0, 1.0, 2.0, 3.0, 4.0])
y = np.array([1.0, 1.0, 2.0, 3.0, 4.0, 4.0])
outp = SlitScan.guess()(x, y)

assert 3.0 == pytest.approx(outp["inflections_diff"].value, rel=1e-2)
assert 1.666666666 == pytest.approx(outp["inflections_diff"].value, rel=1e-2)

def test_guess_inflections_diff_with_all_zero_data(self):
x = np.array([-1.0, 0.0, 1.0, 2.0, 3.0, 4.0])
y = np.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0])
outp = SlitScan.guess()(x, y)

assert 1.0 == pytest.approx(outp["inflections_diff"].value, rel=1e-2)
assert 1.666666666 == pytest.approx(outp["inflections_diff"].value, rel=1e-2)

def test_guess_height_above_inflection1(self):
x = np.array([-1.0, 0.0, 1.0, 2.0, 3.0, 4.0])
y = np.array([1.0, 1.0, 2.0, 3.0, 4.0, 4.0])
outp = SlitScan.guess()(x, y)

assert 1.0 == pytest.approx(outp["height_above_inflection1"].value, rel=1e-2)
assert 0.6 == pytest.approx(outp["height_above_inflection1"].value, rel=1e-2)

def test_guess_inflection0(self):
x = np.arange(-5.0, 5.0, 1.0)
Expand Down
24 changes: 8 additions & 16 deletions tests/devices/simpledae/test_controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,51 +44,43 @@ async def test_period_per_point_controller_begins_run_in_setup_and_ends_in_teard
set_mock_value(simpledae.run_state, RunstateEnum.PAUSED)
await period_per_point_controller.setup(simpledae)
get_mock_put(simpledae.controls.begin_run_ex._raw_begin_run_ex).assert_called_once_with(
BeginRunExBits.BEGIN_PAUSED, wait=True, timeout=None
BeginRunExBits.BEGIN_PAUSED, wait=True
)
set_mock_value(simpledae.run_state, RunstateEnum.SETUP)
await period_per_point_controller.teardown(simpledae)
get_mock_put(simpledae.controls.end_run).assert_called_once_with(None, wait=True, timeout=None)
get_mock_put(simpledae.controls.end_run).assert_called_once_with(None, wait=True)


async def test_aborting_period_per_point_controller_aborts_in_teardown(
simpledae: SimpleDae, aborting_period_per_point_controller: PeriodPerPointController
):
set_mock_value(simpledae.run_state, RunstateEnum.SETUP)
await aborting_period_per_point_controller.teardown(simpledae)
get_mock_put(simpledae.controls.abort_run).assert_called_once_with(
None, wait=True, timeout=None
)
get_mock_put(simpledae.controls.abort_run).assert_called_once_with(None, wait=True)


async def test_period_per_point_controller_changes_periods_and_counts(
simpledae: SimpleDae, period_per_point_controller: PeriodPerPointController
):
set_mock_value(simpledae.run_state, RunstateEnum.RUNNING)
await period_per_point_controller.start_counting(simpledae)
get_mock_put(simpledae.controls.resume_run).assert_called_once_with(
None, wait=True, timeout=None
)
get_mock_put(simpledae.period_num).assert_called_once_with(1, wait=True, timeout=None)
get_mock_put(simpledae.controls.resume_run).assert_called_once_with(None, wait=True)
get_mock_put(simpledae.period_num).assert_called_once_with(1, wait=True)

set_mock_value(simpledae.run_state, RunstateEnum.PAUSED)
await period_per_point_controller.stop_counting(simpledae)
get_mock_put(simpledae.controls.pause_run).assert_called_once_with(
None, wait=True, timeout=None
)
get_mock_put(simpledae.controls.pause_run).assert_called_once_with(None, wait=True)


async def test_run_per_point_controller_starts_and_ends_runs(
simpledae: SimpleDae, run_per_point_controller: RunPerPointController
):
set_mock_value(simpledae.run_state, RunstateEnum.RUNNING)
await run_per_point_controller.start_counting(simpledae)
get_mock_put(simpledae.controls.begin_run).assert_called_once_with(
None, wait=True, timeout=None
)
get_mock_put(simpledae.controls.begin_run).assert_called_once_with(None, wait=True)

await run_per_point_controller.stop_counting(simpledae)
get_mock_put(simpledae.controls.end_run).assert_called_once_with(None, wait=True, timeout=None)
get_mock_put(simpledae.controls.end_run).assert_called_once_with(None, wait=True)


async def test_run_per_point_controller_publishes_run(
Expand Down
Loading

0 comments on commit 4fb04b2

Please sign in to comment.