From af111413c70de98de101a194b0f63d1c2fe1bbe6 Mon Sep 17 00:00:00 2001 From: MDecarabas <72196105+MDecarabas@users.noreply.github.com> Date: Fri, 12 Apr 2024 16:33:04 -0500 Subject: [PATCH] Edge align Function created (#943) * Added edge_align plan for beam edge alignment * Need to find a better way to check signal quality. * Formatted using ruff * small formatting changes * removed unused stats import * removed another unused import * Added a check function that yields correct results * intermediate commit saving progress * finished try/except when bad signal is detected * Fixed formatting * Added scipy to environment.yaml & requirements.txt * CI #909 switch to 'ruff' * DOC #909 update release notes * MNT #909 revise per 'ruff check' * CI #909 update * Bump davidslusser/actions_python_ruff from 1.0.0 to 1.0.1 Bumps [davidslusser/actions_python_ruff](https://github.com/davidslusser/actions_python_ruff) from 1.0.0 to 1.0.1. - [Release notes](https://github.com/davidslusser/actions_python_ruff/releases) - [Commits](https://github.com/davidslusser/actions_python_ruff/compare/v1.0.0...v1.0.1) --- updated-dependencies: - dependency-name: davidslusser/actions_python_ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * MNT #872 hoist from MPE devices * TST #872 adjust for new hoist * MNT #872 * MNT #872 refactor * MNT #872 hoist utils * DOC #912 * ENV #938 bump the minimum requirement * DOC #932 update relase notes * DOC #938 contribution from @sveseli * MNT #914 setup.py --> pyproject.toml * DOC #921 copyright year * MNT #914 * Added edge_align function for beam edge alignment * added scipy to pyproject.toml * CI #909 switch to 'ruff' * DOC badge * DOC badge * DOC #940 example scans with sscan record * DOC #940 it's only a word ... * DOC #940 * DOC #940 it's a demo, actually * DOC #940 * DOC #940 convert the blocking call to st.wait() * DOC #940 per review * DOC #940 per review * DOC #940 proofreading * DOC #940 per review, remove the section with the polling loop * DOC #940 new title * DOC #940 more proofing * DOC #940 clarify the default sscan record array size of 1,000 * DOC #940 refactor to polling loop. Show the recommended style in a details section. * DOC #940 * Added put_complete=True to sensitivity value and unit for the SRS-570 Pre-amp. * Added a test for the sensitivity_unit put_complete, and removed the put_complete from sensitivity_value. * CI #955 move requirements file * MNT #955 sphinx config * DOC #955 re-arrange * DOC #955 apply to lower-level pages * DOC #955 add docs upload * CI #955 * CI #955 * CI #955 wip * CI #955 * CI #955 * DOC #917 release note * DOC #957 merge overview into home page * DOC #955 * DOC #955 * DOC #955 * DOC #955 * Added edge_align function for beam edge alignment * Added edge_align function for beam edge alignment * Added edge_align function for beam edge alignment * DOC #909 update release notes * DOC #932 update relase notes * Added edge_align function for beam edge alignment * CI #909 switch to 'ruff' * DOC #955 * Fixed merge issue * fixed unused import * change for tests to pass * Formatting changes * Fixed repeated section * Small formatting changes * Small syntax change * Added toolz & scipy to conf.py --------- Signed-off-by: dependabot[bot] Co-authored-by: Codrea Co-authored-by: Pete R Jemian Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Pete R Jemian Co-authored-by: Mark Wolfman --- .ruff.toml | 81 ++++++++++++++++++++++++++ CHANGES.rst | 7 ++- apstools/plans/__init__.py | 1 + apstools/plans/alignment.py | 112 ++++++++++++++++++++++++++++++++++++ docs/source/api/_plans.rst | 1 + docs/source/conf.py | 2 + environment.yml | 1 + pyproject.toml | 1 + requirements.txt | 1 + 9 files changed, 204 insertions(+), 3 deletions(-) create mode 100644 .ruff.toml diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 000000000..b03f2a21a --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,81 @@ +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "site-packages", + "venv", +] + +# Same as Black. +line-length = 115 +indent-width = 4 + +# Assume Python 3.8 +target-version = "py38" + +[lint] +# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. +# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or +# McCabe complexity (`C901`) by default. +select = ["E4", "E7", "E9", "F"] +ignore = ["E402", "E741", "F405"] + +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +unfixable = [] + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[lint.per-file-ignores] +"__init__.py" = ["F401", "F403"] +"**/{tests,docs,tools}/*" = ["E402"] + +[format] +# Like Black, use double quotes for strings. +quote-style = "double" + +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" + +# Enable auto-formatting of code examples in docstrings. Markdown, +# reStructuredText code/literal blocks and doctests are all supported. +# +# This is currently disabled by default, but it is planned for this +# to be opt-out in the future. +docstring-code-format = false + +# Set the line length limit used when formatting code snippets in +# docstrings. +# +# This only has an effect when the `docstring-code-format` setting is +# enabled. +docstring-code-line-length = "dynamic" diff --git a/CHANGES.rst b/CHANGES.rst index 5df5cddd8..0a7afeb98 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -31,10 +31,11 @@ describe future plans. release expected by 2024-04-12 New Features - ------------ +------------ + +* Add new plan for edge alignment called edge_align +* Added a mesh grid scan plan that will collect until number of collection points is met - * Added a mesh grid scan plan that will collect until number of collection points is met - Fixes ----- diff --git a/apstools/plans/__init__.py b/apstools/plans/__init__.py index abadee10f..0d652387c 100644 --- a/apstools/plans/__init__.py +++ b/apstools/plans/__init__.py @@ -2,6 +2,7 @@ from .alignment import TuneResults from .alignment import lineup from .alignment import lineup2 +from .alignment import edge_align from .alignment import tune_axes from .command_list import CommandFileReadError from .command_list import command_list_as_table diff --git a/apstools/plans/alignment.py b/apstools/plans/alignment.py index ce5b19f71..0ad7d295f 100644 --- a/apstools/plans/alignment.py +++ b/apstools/plans/alignment.py @@ -16,6 +16,9 @@ import numpy as np import pyRestTable +from scipy.optimize import curve_fit +from scipy.special import erf + from bluesky import plan_stubs as bps from bluesky import plans as bp from bluesky import preprocessors as bpp @@ -213,6 +216,115 @@ def peak_analysis(): scaler.stage_sigs = old_sigs +def edge_align(detectors, mover, start, end, points, cat=None, md={}): + """ + Align to the edge given mover & detector data, relative to absolute position. + + This plan can be used in the queueserver, Jupyter notebooks, and IPython + consoles. + + PARAMETERS + ---------- + detectors *Readable* or [*Readable*]: + Detector object or list of detector objects (each is a Device or + Signal). + + mover *Movable*: + Mover object, such as motor or other positioner. + + start *float*: + Starting point for the scan. This is an absolute motor location. + + end *float*: + Ending point for the scan. This is an absolute motor location. + + points *int*: + Number of points in the scan. + + cat *databroker.temp().v2*: + Catalog where bluesky data is saved and can be retrieved from. + + md *dict*: + User-supplied metadata for this scan. + """ + + def guess_erf_params(x_data, y_data): + """ + Provide an initial guess for the parameters of an error function. + + Parameters + ---------- + x_data : A numpy array of the values on the x_axis + y_data : A numpy array of the values on the y_axis + + Returns + ------- + guess : dict + A dictionary containing the guessed parameters 'low_y_data', 'high_y_data', 'width', and 'midpoint'. + """ + + # Sort data to make finding the mid-point easier and to assist in other estimations + y_data_sorted = np.sort(y_data) + x_data_sorted = np.sort(x_data) + + # Estimate low and high as the first and last elements (assuming sorted data) + low_y_data = np.min(y_data_sorted) + high_y_data = np.max(y_data_sorted) + + low_x_data = np.min(x_data_sorted) + high_x_data = np.max(x_data_sorted) + + # Estimate wid as a fraction of the range. This is very arbitrary and might need tuning! + width = ( + high_x_data - low_x_data + ) / 10 # This is a guess and might need adjustment based on your data's characteristics + + # Estimate the midpoint of the x values + midpoint = x_data[int(len(x_data) / 2)] + + return [low_y_data, high_y_data, width, midpoint] + + def erf_model(x, low, high, width, midpoint): + """ + Create error function for fitting and simulation + + Parameters + ---------- + x : input upon which error function is evaluated + low : min value of error function + high : max value of error function + width : "spread" of error function transition region + midpoint: location of error function's "center" + """ + return (high - low) * 0.5 * (1 - erf((x - midpoint) / width)) + low + + if not isinstance(detectors, (tuple, list)): + detectors = [detectors] + + _md = dict(purpose="edge_align") + _md.update(md or {}) + + uid = yield from bp.scan(detectors, mover, start, end, points, md=_md) + cat = cat or utils.getCatalog() + run = cat[uid] # return uids + ds = run.primary.read() + + x = ds["mover"] + y = ds["noisy"] + + try: + initial_guess = guess_erf_params(x, y) + popt, pcov = curve_fit(erf_model, x, y, p0=initial_guess) + if pcov[3, 3] != np.inf: + print("Significant signal change detected; motor moving to detected edge.") + yield from bps.mv(mover, popt[3]) + else: + raise Exception + except Exception as reason: + print(f"reason: {reason}") + print("No significant signal change detected; motor movement skipped.") + + def lineup2( # fmt: off detectors, mover, rel_start, rel_end, points, diff --git a/docs/source/api/_plans.rst b/docs/source/api/_plans.rst index 2752215e9..d87e4f560 100644 --- a/docs/source/api/_plans.rst +++ b/docs/source/api/_plans.rst @@ -31,6 +31,7 @@ Custom Scans .. autosummary:: + ~apstools.plans.alignment.edge_align ~apstools.plans.alignment.lineup ~apstools.plans.alignment.lineup2 ~apstools.plans.alignment.tune_axes diff --git a/docs/source/conf.py b/docs/source/conf.py index fec3591e4..a6fc50829 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -110,6 +110,8 @@ psutil pyRestTable pysumreg + scipy spec2nexus + toolz xarray """.split() diff --git a/environment.yml b/environment.yml index cfc4e86e8..a628ccb9d 100644 --- a/environment.yml +++ b/environment.yml @@ -31,6 +31,7 @@ dependencies: - pysumreg - qt =5 - readline !=8.1.2 + - scipy - setuptools-scm - spec2nexus - sphinx >=5 diff --git a/pyproject.toml b/pyproject.toml index e7786ef80..b8070f750 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,6 +67,7 @@ dependencies = [ "pyepics>=3.4.2", "pyRestTable>=2020.0.8", "pysumreg", + "scipy", "spec2nexus>=2021.1.7", "toolz>=0.12.1", "xlrd", diff --git a/requirements.txt b/requirements.txt index ec3a919d3..867acfd22 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,5 +13,6 @@ psutil pyepics>=3.4.2 pyRestTable pysumreg +scipy spec2nexus>=2021.1.7 xlrd