Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve diagnostics & reporting of lineup2() plan #1045

Merged
merged 12 commits into from
Dec 5, 2024
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