Skip to content

Commit

Permalink
Merge branch 'main' into 38_generate_files_containing_fit_results
Browse files Browse the repository at this point in the history
  • Loading branch information
jackbdoughty authored Dec 3, 2024
2 parents df3c8d4 + e48d253 commit 580c9f1
Show file tree
Hide file tree
Showing 34 changed files with 409 additions and 111 deletions.
26 changes: 17 additions & 9 deletions doc/callbacks/plotting.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,23 @@ to plot a scan with a logarithmically-scaled y-axis:
```python
import matplotlib.pyplot as plt
from ibex_bluesky_core.callbacks.plotting import LivePlot
# Create a new figure to plot onto.
plt.figure()
# Make a new set of axes on that figure
ax = plt.gca()
# Set the y-scale to logarithmic
ax.set_yscale("log")
# Use the above axes in a LivePlot callback
plot_callback = LivePlot(y="y_variable", x="x_variable", ax=ax, yerr="yerr_variable")
# yerr is the uncertanties of each y value, producing error bars
from ibex_bluesky_core.plan_stubs import call_qt_aware

def plan():
# Create a new figure to plot onto.
yield from call_qt_aware(plt.figure)
# Make a new set of axes on that figure
ax = yield from call_qt_aware(plt.gca)
# Set the y-scale to logarithmic
yield from call_qt_aware(ax.set_yscale, "log")
# Use the above axes in a LivePlot callback
plot_callback = LivePlot(y="y_variable", x="x_variable", ax=ax, yerr="yerr_variable")
# yerr is the uncertanties of each y value, producing error bars
```

```{note}
See [docs for `call_qt_aware`](../plan_stubs/matplotlib_helpers.md) for a description of why we need to use
`yield from call_qt_aware` rather than calling `matplotlib` functions directly.
```

By providing a signal name to the `yerr` argument you can pass uncertainties to LivePlot, by not providing anything for this argument means that no errorbars will be drawn. Errorbars are drawn after each point collected, displaying their standard deviation- uncertainty data is collected from Bluesky event documents and errorbars are updated after every new point added.
Expand Down
6 changes: 5 additions & 1 deletion doc/devices/blocks.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,16 @@ from ibex_bluesky_core.devices.block import block_mot
mot_block = block_mot("motor_block")
```

A motor block does not need an explicit write config: it always waits for the requested motion
to complete. See {py:obj}`ibex_bluesky_core.devices.block.BlockMot` for a detailed mapping of
the usual write-configuration options and how these are instead achieved by a motor block.

## Configuring block write behaviour

`BlockRw` and `BlockRwRbv` both take a `write_config` argument, which can be used to configure
the behaviour on writing to a block, for example tolerances and settle times.

See the docstring on `ibex_bluesky_core.devices.block.BlockWriteConfig` for a detailed
See {py:class}`ibex_bluesky_core.devices.block.BlockWriteConfig` for a detailed
description of all the options which are available.

## Run control
Expand Down
50 changes: 50 additions & 0 deletions doc/plan_stubs/matplotlib_helpers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# `call_qt_aware` (matplotlib helpers)

When attempting to use `matplotlib` UI functions directly in a plan, and running `matplotlib` using a `Qt`
backend (e.g. in a standalone shell outside IBEX), you may see a hang or an error of the form:

```
UserWarning: Starting a Matplotlib GUI outside of the main thread will likely fail.
fig, ax = plt.subplots()
```

This is because the `RunEngine` runs plans in a worker thread, not in the main thread, which then requires special
handling when calling functions that will update a UI.

The {py:obj}`ibex_bluesky_core.plan_stubs.call_qt_aware` plan stub can call `matplotlib` functions in a
Qt-aware context, which allows them to be run directly from a plan. It allows the same arguments and
keyword-arguments as the underlying matplotlib function it is passed.

```{note}
Callbacks such as `LivePlot` and `LiveFitPlot` already route UI calls to the appropriate UI thread by default.
The following plan stubs are only necessary if you need to call functions which will create or update a matplotlib
plot from a plan directly - for example to create or close a set of axes before passing them to callbacks.
```

Usage example:

```python
import matplotlib.pyplot as plt
from ibex_bluesky_core.plan_stubs import call_qt_aware
from ibex_bluesky_core.callbacks.plotting import LivePlot
from bluesky.callbacks import LiveFitPlot
from bluesky.preprocessors import subs_decorator


def my_plan():
# BAD - likely to either crash or hang the plan.
# plt.close("all")
# fig, ax = plt.subplots()

# GOOD
yield from call_qt_aware(plt.close, "all")
fig, ax = yield from call_qt_aware(plt.subplots)

# Pass the matplotlib ax object to other callbacks
@subs_decorator([
LiveFitPlot(..., ax=ax),
LivePlot(..., ax=ax),
])
def inner_plan():
...
```
6 changes: 4 additions & 2 deletions manual_system_tests/dae_scan.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""Demonstration plan showing basic bluesky functionality."""

import os
from collections.abc import Generator
from pathlib import Path
from typing import Generator

import bluesky.plan_stubs as bps
import bluesky.plans as bp
Expand All @@ -28,6 +28,7 @@
GoodFramesNormalizer,
)
from ibex_bluesky_core.devices.simpledae.waiters import GoodFramesWaiter
from ibex_bluesky_core.plan_stubs import call_qt_aware
from ibex_bluesky_core.run_engine import get_run_engine

NUM_POINTS: int = 3
Expand Down Expand Up @@ -72,7 +73,8 @@ def dae_scan_plan() -> Generator[Msg, None, None]:
controller.run_number.set_name("run number")
reducer.intensity.set_name("normalized counts")

_, ax = plt.subplots()
_, ax = yield from call_qt_aware(plt.subplots)

lf = LiveFit(
Linear.fit(), y=reducer.intensity.name, x=block.name, yerr=reducer.intensity_stddev.name
)
Expand Down
2 changes: 1 addition & 1 deletion manual_system_tests/interruption.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Demonstration plan showing basic bluesky functionality."""

import os
from typing import Generator
from collections.abc import Generator

import bluesky.plan_stubs as bps
from bluesky.utils import Msg
Expand Down
49 changes: 35 additions & 14 deletions ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,56 @@ line-length = 100
indent-width = 4

[lint]
preview = true
extend-select = [
"N", # pep8-naming
"D", # pydocstyle (can use this later but for now causes too many errors)
"D", # pydocstyle
"I", # isort (for imports)
"E501", # Line too long ({width} > {limit})
"E",
"F",
"W",
"ANN",
"ASYNC", # Asyncio-specific checks
"B",
"NPY", # Numpy-specific rules
"RUF", # Ruff-specific checks, include some useful asyncio rules
"E", # Pycodestyle errors
"W", # Pycodestyle warnings
"F", # Pyflakes
"PL", # Pylint
"B", # Flake8-bugbear
"PIE", # Flake8-pie
"ANN", # Annotations
"ASYNC", # Asyncio-specific checks
"NPY", # Numpy-specific rules
"RUF", # Ruff-specific checks, include some useful asyncio rules
"FURB", # Rules from refurb
"ERA", # Commented-out code
"PT", # Pytest-specific rules
"LOG", # Logging-specific rules
"G", # Logging-specific rules
"UP", # Pyupgrade
"SLF", # Private-member usage
"PERF", # Performance-related rules
]
ignore = [
"D406", # Section name should end with a newline ("{name}")
"D407", # Missing dashed underline after section ("{name}")
"D213", # Incompatible with D212
"D203", # Incompatible with D211
"D213", # Incompatible with D212
"D203", # Incompatible with D211
"B901", # This is a standard, expected, pattern in bluesky
"PLR6301" # Too noisy
]
[lint.per-file-ignores]
"tests/*" = [
"N802",
"D", # Don't require method documentation for test methods
"ANN" # Don't require tests to use type annotations
"N802", # Allow test names to be long / not pep8
"D", # Don't require method documentation for test methods
"ANN", # Don't require tests to use type annotations
"PLR2004", # Allow magic numbers in tests
"PLR0915", # Allow complex tests
"PLR0914", # Allow complex tests
"PLC2701", # Allow tests to import "private" things
"SLF001", # Allow tests to use "private" things
]
"doc/conf.py" = [
"D100"
]

[lint.pep8-naming]
extend-ignore-names = ["RE"] # Conventional name used for RunEngine

[lint.pylint]
max-args = 6
2 changes: 1 addition & 1 deletion src/ibex_bluesky_core/callbacks/document_logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,5 @@ def __call__(self, name: str, document: dict[str, Any]) -> None:

to_write: dict[str, Any] = {"type": name, "document": document}

with open(self.filename, "a") as outfile:
with open(self.filename, "a", encoding="utf8") as outfile:
outfile.write(f"{json.dumps(to_write)}\n")
4 changes: 2 additions & 2 deletions src/ibex_bluesky_core/callbacks/file_logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def start(self, doc: RunStart) -> None:
)
header_data[START_TIME] = formatted_time

with open(self.filename, "a", newline="") as outfile:
with open(self.filename, "a", newline="", encoding="utf-8") as outfile:
for key, value in header_data.items():
outfile.write(f"{key}: {value}\n")

Expand Down Expand Up @@ -102,7 +102,7 @@ def event(self, doc: Event) -> Event:
else value
)

with open(self.filename, "a", newline="") as outfile:
with open(self.filename, "a", newline="", encoding="utf-8") as outfile:
file_delimiter = ","
if doc[SEQ_NUM] == 1:
# If this is the first event, write out the units before writing event data.
Expand Down
7 changes: 3 additions & 4 deletions src/ibex_bluesky_core/callbacks/fitting/fitting_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ def model(cls, *args: int) -> lmfit.Model:
(x-values: NDArray, parameters: np.float64 -> y-values: NDArray)
"""
pass

@classmethod
@abstractmethod
Expand All @@ -49,7 +48,6 @@ def guess(
(x-values: NDArray, y-values: NDArray -> parameters: Dict[str, lmfit.Parameter])
"""
pass

@classmethod
def fit(cls, *args: int) -> FitMethod:
Expand Down Expand Up @@ -208,8 +206,9 @@ class Polynomial(Fit):
@classmethod
def _check_degree(cls, args: tuple[int, ...]) -> int:
"""Check that polynomial degree is valid."""
degree = args[0] if args else 7
if not (0 <= degree <= 7):
max_degree = 7
degree = args[0] if args else max_degree
if not (0 <= degree <= max_degree):
raise ValueError("The polynomial degree should be at least 0 and smaller than 8.")
return degree

Expand Down
2 changes: 1 addition & 1 deletion src/ibex_bluesky_core/callbacks/plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def __init__(
"""
super().__init__(y=y, x=x, *args, **kwargs) # noqa: B026
if yerr is not None:
self.yerr, *others = get_obj_fields([yerr])
self.yerr, *_others = get_obj_fields([yerr])
else:
self.yerr = None
self.yerr_data = []
Expand Down
6 changes: 3 additions & 3 deletions src/ibex_bluesky_core/devices/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import binascii
import os
import zlib
from typing import Type, TypeVar
from typing import TypeVar

from ophyd_async.core import SignalDatatype, SignalRW
from ophyd_async.epics.core import epics_signal_rw
Expand All @@ -18,7 +18,7 @@ def get_pv_prefix() -> str:
prefix = os.getenv("MYPVPREFIX")

if prefix is None:
raise EnvironmentError("MYPVPREFIX environment variable not available - please define")
raise OSError("MYPVPREFIX environment variable not available - please define")

return prefix

Expand Down Expand Up @@ -48,7 +48,7 @@ def compress_and_hex(value: str) -> bytes:
return binascii.hexlify(compr)


def isis_epics_signal_rw(datatype: Type[T], read_pv: str, name: str = "") -> SignalRW[T]:
def isis_epics_signal_rw(datatype: type[T], read_pv: str, name: str = "") -> SignalRW[T]:
"""Make a RW signal with ISIS' PV naming standard ie. read_pv as TITLE, write_pv as TITLE:SP."""
write_pv = f"{read_pv}:SP"
return epics_signal_rw(datatype, read_pv, write_pv, name)
Loading

0 comments on commit 580c9f1

Please sign in to comment.