Skip to content

Commit c329bad

Browse files
committed
Add additional explanations
1 parent 9d0ceea commit c329bad

File tree

6 files changed

+85
-55
lines changed

6 files changed

+85
-55
lines changed

src/ibex_bluesky_core/callbacks/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@
1616
from event_model import RunStart
1717
from matplotlib.axes import Axes
1818

19+
from ibex_bluesky_core.callbacks._centre_of_mass import (
20+
CentreOfMass,
21+
)
1922
from ibex_bluesky_core.callbacks._document_logger import DocLoggingCallback
2023
from ibex_bluesky_core.callbacks._file_logger import (
2124
HumanReadableFileCallback,
2225
)
2326
from ibex_bluesky_core.callbacks._fitting import (
24-
CentreOfMass,
2527
ChainedLiveFit,
2628
LiveFit,
2729
LiveFitLogger,
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import logging
2+
3+
import numpy as np
4+
from bluesky.callbacks import CollectThenCompute
5+
6+
from ibex_bluesky_core.utils import center_of_mass_of_area_under_curve
7+
8+
logger = logging.getLogger(__name__)
9+
10+
__all__ = ["CentreOfMass"]
11+
12+
13+
class CentreOfMass(CollectThenCompute):
14+
"""Compute centre of mass after a run finishes.
15+
16+
Calculates the CoM of the 2D region bounded by min(y), min(x), max(x),
17+
and straight-line segments joining (x, y) data points with their nearest
18+
neighbours along the x axis.
19+
"""
20+
21+
def __init__(self, x: str, y: str) -> None:
22+
"""Initialise the callback.
23+
24+
Args:
25+
x: Name of independent variable in event data
26+
y: Name of dependent variable in event data
27+
28+
"""
29+
super().__init__()
30+
self.x: str = x
31+
self.y: str = y
32+
self._result: float | None = None
33+
34+
@property
35+
def result(self) -> float | None:
36+
"""The centre-of-mass calculated by this callback."""
37+
return self._result
38+
39+
def compute(self) -> None:
40+
"""Calculate statistics at the end of the run."""
41+
x_values = []
42+
y_values = []
43+
44+
for event in self._events:
45+
if self.x not in event["data"]:
46+
raise ValueError(f"{self.x} is not in event document.")
47+
48+
if self.y not in event["data"]:
49+
raise ValueError(f"{self.y} is not in event document.")
50+
51+
x_values.append(event["data"][self.x])
52+
y_values.append(event["data"][self.y])
53+
54+
if not x_values:
55+
return
56+
57+
x_data = np.array(x_values, dtype=np.float64)
58+
y_data = np.array(y_values, dtype=np.float64)
59+
(self._result, _) = center_of_mass_of_area_under_curve(x_data, y_data)

src/ibex_bluesky_core/callbacks/_fitting.py

Lines changed: 2 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import numpy as np
1212
from bluesky.callbacks import CallbackBase, LiveFitPlot
1313
from bluesky.callbacks import LiveFit as _DefaultLiveFit
14-
from bluesky.callbacks.core import CollectThenCompute, make_class_safe
14+
from bluesky.callbacks.core import make_class_safe
1515
from event_model import Event, EventDescriptor, RunStart, RunStop
1616
from lmfit import Parameter
1717
from matplotlib.axes import Axes
@@ -26,11 +26,10 @@
2626
get_instrument,
2727
)
2828
from ibex_bluesky_core.fitting import FitMethod
29-
from ibex_bluesky_core.utils import center_of_mass_of_area_under_curve
3029

3130
logger = logging.getLogger(__name__)
3231

33-
__all__ = ["CentreOfMass", "ChainedLiveFit", "LiveFit", "LiveFitLogger"]
32+
__all__ = ["ChainedLiveFit", "LiveFit", "LiveFitLogger"]
3433

3534

3635
@make_class_safe(logger=logger) # pyright: ignore (pyright doesn't understand this decorator)
@@ -256,54 +255,6 @@ def write_fields_table_uncertainty(self) -> None:
256255
self.csvwriter.writerows(rows)
257256

258257

259-
class CentreOfMass(CollectThenCompute):
260-
"""Compute centre of mass after a run finishes.
261-
262-
Calculates the CoM of the 2D region bounded by min(y), min(x), max(x),
263-
and straight-line segments joining (x, y) data points with their nearest
264-
neighbours along the x axis.
265-
"""
266-
267-
def __init__(self, x: str, y: str) -> None:
268-
"""Initialise the callback.
269-
270-
Args:
271-
x: Name of independent variable in event data
272-
y: Name of dependent variable in event data
273-
274-
"""
275-
super().__init__()
276-
self.x: str = x
277-
self.y: str = y
278-
self._result: float | None = None
279-
280-
@property
281-
def result(self) -> float | None:
282-
return self._result
283-
284-
def compute(self) -> None:
285-
"""Calculate statistics at the end of the run."""
286-
x_values = []
287-
y_values = []
288-
289-
for event in self._events:
290-
if self.x not in event["data"]:
291-
raise OSError(f"{self.x} is not in event document.")
292-
293-
if self.y not in event["data"]:
294-
raise OSError(f"{self.y} is not in event document.")
295-
296-
x_values.append(event["data"][self.x])
297-
y_values.append(event["data"][self.y])
298-
299-
if not x_values:
300-
return
301-
302-
x_data = np.array(x_values, dtype=np.float64)
303-
y_data = np.array(y_values, dtype=np.float64)
304-
(self._result, _) = center_of_mass_of_area_under_curve(x_data, y_data)
305-
306-
307258
class ChainedLiveFit(CallbackBase):
308259
"""Processes multiple LiveFits, each fit's results inform the next, with optional plotting.
309260

src/ibex_bluesky_core/utils.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,17 +73,35 @@ def center_of_mass_of_area_under_curve(
7373
Returns a tuple of the centre of mass and the total area under the curve.
7474
7575
"""
76+
# Sorting here avoids special-cases with disordered points, which may occur
77+
# from a there-and-back scan, or from an adaptive scan.
7678
sort_indices = np.argsort(x, kind="stable")
7779
x = np.take_along_axis(x, sort_indices, axis=None)
7880
y = np.take_along_axis(y - np.min(y), sort_indices, axis=None)
81+
82+
# If the data points are "fence-posts", this calculates the x width of
83+
# each "fence panel".
7984
widths = np.diff(x)
8085

8186
# Area under the curve for two adjacent points is a right trapezoid.
8287
# Split that trapezoid into a rectangular region, plus a right triangle.
8388
# Find area and effective X CoM for each.
89+
90+
# We want the area of the rectangular part of the right trapezoid.
91+
# This is width * [height of either left or right point, whichever is lowest]
8492
rect_areas = widths * np.minimum(y[:-1], y[1:])
93+
# CoM of a rectangle in x is simply the average x.
8594
rect_x_com = (x[:-1] + x[1:]) / 2.0
95+
96+
# Now the area of the triangular part of the right trapezoid - this is
97+
# width * height / 2, where height is the absolute difference between the
98+
# two y values.
8699
triangle_areas = widths * np.abs(y[:-1] - y[1:]) / 2.0
100+
# CoM of a right triangle is 1/3 along the base, from the right angle
101+
# y[:-1] > y[1:] is true if y_[n] > y_[n+1], (i.e. if the right angle is on the
102+
# left-hand side of the triangle).
103+
# If that's true, the CoM lies 1/3 of the way along the x axis
104+
# Otherwise, the CoM lies 2/3 of the way along the x axis (1/3 from the right angle)
87105
triangle_x_com = np.where(
88106
y[:-1] > y[1:], x[:-1] + (widths / 3.0), x[:-1] + (2.0 * widths / 3.0)
89107
)

tests/callbacks/fitting/test_centre_of_mass.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ def test_error_thrown_if_no_x_data_in_event():
9494
}
9595
)
9696

97-
with pytest.raises(OSError, match=r"motor is not in event document."):
97+
with pytest.raises(ValueError, match=r"motor is not in event document."):
9898
com.compute()
9999

100100

@@ -108,5 +108,5 @@ def test_error_thrown_if_no_y_data_in_event():
108108
}
109109
)
110110

111-
with pytest.raises(OSError, match=r"invariant is not in event document."):
111+
with pytest.raises(ValueError, match=r"invariant is not in event document."):
112112
com.compute()

tests/callbacks/test_isis_callbacks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def test_peak_stats_without_peak_stats_callback_raises():
3131
).peak_stats
3232

3333

34-
def test_centre_of_mass_centre_of_mass_callback_raises():
34+
def test_GIVEN_centre_of_mass_callback_not_added_WHEN_getting_com_from_isiscallbacks_THEN_raises():
3535
with pytest.raises(
3636
ValueError,
3737
match=r"centre of mass was not added as a callback.",

0 commit comments

Comments
 (0)