Skip to content

Commit

Permalink
Merge pull request #1045 from BCDA-APS/1044-lineup2-diagnostics
Browse files Browse the repository at this point in the history
Improve diagnostics & reporting of lineup2() plan
  • Loading branch information
prjemian authored Dec 5, 2024
2 parents e9780ed + 69479a1 commit 7fcb7c4
Show file tree
Hide file tree
Showing 22 changed files with 997 additions and 118 deletions.
18 changes: 15 additions & 3 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,30 @@ describe future plans.
Enhancements
------------

- Add 'dynamic_import()' (support 'ad_creator()' from device file).
- Add 'utils.xy_statistics()' (support 'plans.lineup2()').
- Add 'utils.dynamic_import()' (support 'devices.ad_creator()' from device file).
- Add 'utils.MMap' (support 'plans.lineup2()').
- Add 'utils.peak_full_width' (support 'plans.lineup2()').

Fixes
-----

- 'PVPositionerSoftDone' used an invalid subscription event type
in unusual cases (with fake ophyd simulated devices).
- In some cases, 'plans.lineup2()' appeared to succeed but motor was not in
aligned position.
- In unusual cases (with fake ophyd simulated devices),
'devices.PVPositionerSoftDone' used an invalid subscription event type.

Maintenance
-----------

- In 'ad_creator()', convert text class name to class object.
- Refactor 'plans.lineup2()': new statistics computations & improve
diagnostics.

Deprecations
-----------

- Use of 'PySumReg.SummationRegisters' to be removed in next major release.

1.7.1
******
Expand Down
1 change: 0 additions & 1 deletion apstools/callbacks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from .nexus_writer import NEXUS_RELEASE
from .nexus_writer import NXWriter
from .nexus_writer import NXWriterAPS
from .scan_signal_statistics import factor_fwhm
from .scan_signal_statistics import SignalStatsCallback
from .spec_file_writer import SCAN_ID_RESET_VALUE
from .spec_file_writer import SPEC_TIME_FORMAT
Expand Down
108 changes: 57 additions & 51 deletions apstools/callbacks/scan_signal_statistics.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,20 @@
"""
Collect statistics on the signals used in 1-D scans.
====================================================
Collect statistics on the first detector used in 1-D scans.
===========================================================
.. autosummary::
~factor_fwhm
~SignalStatsCallback
"""

__all__ = """
factor_fwhm
SignalStatsCallback
""".split()

import math
import logging

import pyRestTable
import pysumreg
import pysumreg # deprecate, will remove in next major version bump

logger = logging.getLogger(__name__)
logger.info(__file__)

factor_fwhm = 2 * math.sqrt(2 * math.log(2))
r"""
FWHM :math:`=2\sqrt{2\ln{2}}\cdot\sigma_c`
see: https://statproofbook.github.io/P/norm-fwhm.html
"""


class SignalStatsCallback:
"""
Expand All @@ -48,6 +34,7 @@ class SignalStatsCallback:
from bluesky import plans as bp
from bluesky import preprocessors as bpp
from apstools.callbacks import SignalStatsCallback
signal_stats = SignalStatsCallback()
Expand All @@ -66,7 +53,7 @@ def _inner():
~receiver
~report
~data_stream
~stop_report
~reporting
.. rubric:: Internal API
.. autosummary::
Expand All @@ -76,21 +63,33 @@ def _inner():
~event
~start
~stop
~analysis
~_data
~_scanning
~_registers
"""

data_stream: str = "primary"
"""RunEngine document with signals to to watch."""

stop_report: bool = True
reporting: bool = True
"""If ``True`` (default), call the ``report()`` method when a ``stop`` document is received."""

_scanning: bool = False
"""Is a run *in progress*?"""

_registers: dict = {}
"""Dictionary (keyed on Signal name) of ``SummationRegister()`` objects."""
"""
Deprecated: Use 'analysis' instead, will remove in next major release.
Dictionary (keyed on Signal name) of ``SummationRegister()`` objects.
"""

_data: dict = {}
"""Arrays of x & y data"""

analysis: object = None
"""Dictionary of statistical array analyses."""

# TODO: What happens when the run is paused?

Expand All @@ -105,10 +104,11 @@ def clear(self):
self._scanning = False
self._detectors = []
self._motor = ""
self._registers = {}
self._registers = {} # deprecated, for removal
self._descriptor_uid = None
self._x_name = None
self._y_names = []
self._data = {}

def descriptor(self, doc):
"""Receives 'descriptor' documents from the RunEngine."""
Expand All @@ -122,11 +122,16 @@ def descriptor(self, doc):

# Pick the first motor signal.
self._x_name = doc["hints"][self._motor]["fields"][0]
self._data[self._x_name] = []

# Get the signals for each detector object.s
for d in self._detectors:
self._y_names += doc["hints"][d]["fields"]
for y_name in doc["hints"][d]["fields"]:
self._y_names.append(y_name)
self._data[y_name] = []

# Keep statistics for each of the Y signals (vs. the one X signal).
# deprecated, for removal
self._registers = {y: pysumreg.SummationRegisters() for y in self._y_names}

def event(self, doc):
Expand All @@ -138,8 +143,12 @@ def event(self, doc):

# Collect the data for the signals.
x = doc["data"][self._x_name]
self._data[self._x_name].append(x)

for yname in self._y_names:
self._registers[yname].add(x, doc["data"][yname])
y = doc["data"][yname]
self._registers[yname].add(x, y) # deprecated, for removal
self._data[yname].append(y)

def receiver(self, key, document):
"""Client method used to subscribe to the RunEngine."""
Expand All @@ -151,35 +160,24 @@ def receiver(self, key, document):

def report(self):
"""Print a table with the collected statistics for each signal."""
if len(self._registers) == 0:
if len(self._data) == 0:
return
keys = "n centroid sigma x_at_max_y max_y min_y mean_y stddev_y".split()

x_name = self._x_name
y_name = self._detectors[0]

keys = """
n centroid x_at_max_y
fwhm variance sigma
min_x mean_x max_x
min_y mean_y max_y
success reasons
""".split()

table = pyRestTable.Table()
if len(keys) <= len(self._registers):
# statistics in the column labels
table.labels = ["detector"] + keys
for yname, stats in self._registers.items():
row = [yname]
for k in keys:
try:
v = getattr(stats, k)
except (ValueError, ZeroDivisionError):
v = 0
row.append(v)
table.addRow(row)
else:
# signals in the column labels
table.labels = ["statistic"] + list(self._registers)
for k in keys:
row = [k]
for stats in self._registers.values():
try:
v = getattr(stats, k)
except (ValueError, ZeroDivisionError):
v = 0
row.append(v)
table.addRow(row)
print(f"Motor: {self._x_name}")
table.labels = "statistic value".split()
table.rows = [(k, self.analysis.get(k, "--")) for k in keys if k in self.analysis]
print(f"Motor: {x_name!r} Detector: {y_name!r}")
print(table)

def start(self, doc):
Expand All @@ -192,8 +190,16 @@ def start(self, doc):

def stop(self, doc):
"""Receives 'stop' documents from the RunEngine."""
from ..utils.statistics import xy_statistics

if not self._scanning:
return
self._scanning = False
if self.stop_report:

self.analysis = xy_statistics(
self._data[self._x_name],
self._data[self._detectors[0]],
)

if self.reporting:
self.report()
Loading

0 comments on commit 7fcb7c4

Please sign in to comment.