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

Updates to PVPostionerSoftDone #1005

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
d1b264a
MNT #975 2M undulator
prjemian Jul 8, 2024
ab0bf00
TST #975
prjemian Jul 8, 2024
5271e4e
TST #975 use dummy PVs for testing
prjemian Jul 9, 2024
7676185
TST #975 increase master timeout
prjemian Jul 9, 2024
cfc83bf
TST #975 generalize
prjemian Jul 9, 2024
55f2301
ENH #976 4M object seems identical to 2M
prjemian Jul 9, 2024
d4592f6
TST #976
prjemian Jul 9, 2024
efdd604
DOC #1002
prjemian Jul 9, 2024
60a4166
MNT #976 re-use rather than repeat
prjemian Jul 9, 2024
d423327
Merge pull request #1002 from BCDA-APS/975-2M-undulator
prjemian Jul 9, 2024
93fe31a
DOC catch-up
prjemian Jul 9, 2024
69bdc95
PKG #993 numpy is pinned
prjemian Jul 9, 2024
5e525a4
release 1.6.20
prjemian Jul 10, 2024
9a5afb9
DOC missed the release with this
prjemian Jul 10, 2024
80bb624
Merge pull request #1 from BCDA-APS/916-PVPositionerSoftDone-update
gfabbris Jul 11, 2024
40acf1e
update positioner_soft_done
gfabbris Jul 11, 2024
c77c503
remove unused import
gfabbris Jul 11, 2024
029d3ef
re-add logger
gfabbris Jul 11, 2024
d0d46af
tweaks and cleanup
gfabbris Jul 11, 2024
633802c
update eurotherm to `use_target`
gfabbris Jul 11, 2024
2b3d5ce
tweak test to use_target=True
gfabbris Jul 11, 2024
a05c3fc
tweaks
gfabbris Jul 11, 2024
4dc5dc3
debug
gfabbris Jul 11, 2024
8bce07e
Force done to be False in the start of the motion
gfabbris Jul 22, 2024
0e32b4a
python 3.8 breaking test
gfabbris Jul 22, 2024
4ef2ab5
add use_target=True to lakeshores
gfabbris Jul 22, 2024
791232a
retrigger checks
gfabbris Aug 26, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/code.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ jobs:
strategy:
matrix:
python-version:
- "3.8"
# - "3.8" # TODO: Breaking
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're really close to EOL for Py3.8. This would push the entire repo to drop it. What breaks for Py3.8 now?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I came across this import error.

- "3.9"
- "3.10"
- "3.11"
Expand Down
33 changes: 22 additions & 11 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,34 @@ Project `milestones <https://github.com/BCDA-APS/apstools/milestones>`_
describe future plans.

..
1.6.20
1.6.21
******

release expected by 2024-07-19
release expected by 2024-08-09

New Features
------------
1.6.20
******

released 2024-07-10

* Add new APS PlanarUndulator device.
* Add new APS Revolver_Undulator device.
* Add new APS STI_Undulator device.
New Features
------------

Maintenance
-----------
* Add new APS PlanarUndulator device.
* Add new APS Revolver_Undulator device.
* Add new APS STI_Undulator device.
* Add new APS Undulator2M device.
* Add new APS Undulator4M device.

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

* Pin numpy<2 because upstream dask package needs a fix.
* Removed ApsUndulator and ApsUndulatorDual devices.
* Describe ``.component_names`` in *What are the objects to control?*
* Pin numpy<2 because upstream dask package needs a fix.
* Removed ApsUndulator and ApsUndulatorDual devices.
* Removed top-level requirements files. They were not used.
* Update APS cycle begin & end dates.
* Update device support for APS machine parameters (current, lifetime, ...).

1.6.19
******
Expand Down
4 changes: 4 additions & 0 deletions apstools/devices/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
from .aps_machine import ApsMachineParametersDevice

from .aps_undulator import PlanarUndulator
from .aps_undulator import Revolver_Undulator
from .aps_undulator import STI_Undulator
from .aps_undulator import Undulator2M
from .aps_undulator import Undulator4M

from .area_detector_support import AD_EpicsFileNameMixin
from .area_detector_support import AD_FrameType_schemes
Expand Down
45 changes: 44 additions & 1 deletion apstools/devices/aps_undulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
~PlanarUndulator
~Revolver_Undulator
~STI_Undulator
~Undulator2M
~Undulator4M

.. note:: The ``ApsUndulator`` and ``ApsUndulatorDual`` device support
classes have been removed. These devices are not used in the APS-U era.
"""

import logging
Expand Down Expand Up @@ -166,7 +171,7 @@ class Revolver_Undulator(ID_Spectrum_Mixin, ID_Controls_Mixin, ID_Misc_Mixin, De
class STI_Undulator(PlanarUndulator):
"""APS Planar Undulator built by STI Optronics.

.. index::
.. index::
Ophyd Device; PlanarUndulator
Ophyd Device; STI_Undulator

Expand All @@ -178,6 +183,44 @@ class STI_Undulator(PlanarUndulator):
"""


class Undulator2M(ID_Spectrum_Mixin, ID_Controls_Mixin, ID_Misc_Mixin, Device):
"""APS 2M Undulator.

.. index::
Ophyd Device; PlanarUndulator
Ophyd Device; Undulator2M

APS Use: 1ID, downstream.

EXAMPLE::

undulator = Undulator2M("S01ID:DSID:", name="undulator")
"""

# PVs not found
busy = None
magnet = None
version_plc = None
version_hpmu = None

done = Component(EpicsSignalRO, "BusyM.VAL", kind="omitted")
done_value = 0


class Undulator4M(Undulator2M):
"""APS 4M Undulator.

.. index::
Ophyd Device; PlanarUndulator
Ophyd Device; Undulator4M

APS Use: 11ID, downstream & upstream.

EXAMPLE::

undulator = Undulator4M("S11ID:DSID:", name="undulator")
"""

# -----------------------------------------------------------------------------
# :author: Pete R. Jemian
# :email: jemian@anl.gov
Expand Down
2 changes: 1 addition & 1 deletion apstools/devices/eurotherm_2216e.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ def __init__(self, prefix="", *, tolerance=1, **kwargs):
readback_pv="ignoreRBV",
setpoint_pv="ignore",
tolerance=tolerance,
update_target=False,
use_target=False,
**kwargs,
)
self.sensor.subscribe(self.cb_sensor)
Expand Down
4 changes: 2 additions & 2 deletions apstools/devices/lakeshore_controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ class LakeShore336_LoopControl(PVPositionerSoftDoneWithStop):

def __init__(self, *args, loop_number=None, timeout=10 * HOUR, **kwargs):
self.loop_number = loop_number
super().__init__(*args, timeout=timeout, tolerance=0.1, readback_pv=f"IN{loop_number}", **kwargs)
super().__init__(*args, timeout=timeout, tolerance=0.1, use_target=True, readback_pv=f"IN{loop_number}", **kwargs)
self._settle_time = 0

@property
Expand Down Expand Up @@ -171,7 +171,7 @@ class LS340_LoopBase(PVPositionerSoftDoneWithStop):

def __init__(self, *args, loop_number=None, timeout=10 * HOUR, **kwargs):
self.loop_number = loop_number
super().__init__(*args, readback_pv="ignore", timeout=timeout, tolerance=0.1, **kwargs)
super().__init__(*args, readback_pv="ignore", timeout=timeout, use_target=True, tolerance=0.1, **kwargs)
self._settle_time = 0

@property
Expand Down
84 changes: 23 additions & 61 deletions apstools/devices/positioner_soft_done.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
from ophyd import FormattedComponent
from ophyd import PVPositioner
from ophyd import Signal
from ophyd.signal import EpicsSignalBase

# from ..tests import timed_pause

Expand All @@ -31,36 +30,6 @@
TARGET_UNDEFINED = "undefined"


class _EpicsPositionerSetpointSignal(EpicsSignal):
"""
Special handling when PVPositionerSoftDone setpoint is changed.

When the setpoint is changed, force`` done=False``. For any move, ``done``
**must** transition to ``!= done_value``, then back to ``done_value``.
Without this response, a small move (within tolerance) will not return.
The ``cb_readback()`` method will compute ``done``.
"""

def put(self, value, *args, **kwargs):
"""Make sure 'done' signal goes False when setpoint is changed by us."""
super().put(value, *args, **kwargs)

self.parent.done.put(not self.parent.done_value)
if self.parent.update_target:
kwargs = {}
if issubclass(self.parent.target.__class__, EpicsSignalBase):
kwargs["wait"] = True # Signal.put() warns if kwargs are given
self.parent.target.put(value, **kwargs)

# def get(self, *args, **kwargs):
# value = super().get(*args, **kwargs)
# if self.parent.update_target:
# target = self.parent.target.get()
# if target != TARGET_UNDEFINED:
# value = target
# return value


class PVPositionerSoftDone(PVPositioner):
"""
PVPositioner that computes ``done`` as a soft signal.
Expand All @@ -84,11 +53,11 @@ class PVPositionerSoftDone(PVPositioner):

Defaults to ``10^(-1*precision)``,
where ``precision = setpoint.precision``.
update_target : bool
``True`` when this object updates the ``target`` Component directly.
use_target : bool
``True`` when this object update the ``target`` Component directly.
Use ``False`` if the ``target`` Component will be updated externally,
such as by the controller when ``target`` is an ``EpicsSignal``.
Defaults to ``True``.
Defaults to ``False``.
kwargs :
Passed to `ophyd.PVPositioner`

Expand Down Expand Up @@ -128,7 +97,7 @@ class PVPositionerSoftDone(PVPositioner):
EpicsSignalRO, "{prefix}{_readback_pv}", kind="hinted", auto_monitor=True
)
setpoint = FormattedComponent(
_EpicsPositionerSetpointSignal, "{prefix}{_setpoint_pv}", kind="normal", put_complete=True
EpicsSignal, "{prefix}{_setpoint_pv}", kind="normal", put_complete=True
)
# fmt: on
done = Component(Signal, value=True, kind="config")
Expand All @@ -146,7 +115,7 @@ def __init__(
readback_pv="",
setpoint_pv="",
tolerance=None,
update_target=True,
use_target=False,
**kwargs,
):
# fmt: off
Expand All @@ -165,10 +134,12 @@ def __init__(
# Make the default alias for the readback the name of the
# positioner itself as in EpicsMotor.
self.readback.name = self.name
self.update_target = update_target
self.use_target = use_target

self.readback.subscribe(self.cb_readback)
self.setpoint.subscribe(self.cb_setpoint)
self.setpoint.subscribe(self.cb_update_target, event_type="setpoint")

# cancel subscriptions before object is garbage collected
weakref.finalize(self.readback, self.readback.unsubscribe_all)
weakref.finalize(self.setpoint, self.setpoint.unsubscribe_all)
Expand All @@ -192,6 +163,9 @@ def actual_tolerance(self):
)
# fmt: on

def cb_update_target(self, value, *args, **kwargs):
self.target.put(value)

def cb_readback(self, *args, **kwargs):
"""
Called when readback changes (EPICS CA monitor event) or on-demand.
Expand All @@ -213,12 +187,17 @@ def cb_setpoint(self, *args, **kwargs):
"""
Called when setpoint changes (EPICS CA monitor event).

This method is called when the setpoint is changed by this code or from
some other EPICS client.
When the setpoint is changed, force`` done=False``. For any move, ``done``
**must** transition to ``!= done_value``, then back to ``done_value``.

The 'done' signal is set to False in the custom
_EpicsPositionerSetpointSignal class.
Without this response, a small move (within tolerance) will not return.
The ``cb_readback()`` method will compute ``done``.

Since other code will also call this method, check the keys in kwargs
and do not react to the "wrong" signature.
"""
if "value" in kwargs and "status" not in kwargs:
self.done.put(not self.done_value)
logger.debug("cb_setpoint: done=%s, setpoint=%s", self.done.get(), self.setpoint.get())

@property
Expand All @@ -233,7 +212,7 @@ def inposition(self):
# Since this method must execute quickly, do NOT force
# EPICS CA gets using `use_monitor=False`.
rb = self.readback.get()
sp = self.setpoint.get()
sp = self.setpoint.get() if self.use_target is False else self.target.get()
tol = self.actual_tolerance
inpos = math.isclose(rb, sp, abs_tol=tol)
logger.debug("inposition: inpos=%s rb=%s sp=%s tol=%s", inpos, rb, sp, tol)
Expand All @@ -246,18 +225,12 @@ def precision(self):
def _setup_move(self, position):
"""Move and do not wait until motion is complete (asynchronous)"""
self.log.debug("%s.setpoint = %s", self.name, position)

# Write the setpoint value.
self.setpoint.put(position, wait=True)
# The 'done' and 'target' signals are handled by
# the custom '_EpicsPositionerSetpointSignal' class.

self.done.put(False)
if self.actuate is not None:
self.log.debug("%s.actuate = %s", self.name, self.actuate_value)
self.actuate.put(self.actuate_value, wait=False)

# Force the first check for done.
self.cb_readback()
self.cb_readback() # This is needed to force the first check.


class PVPositionerSoftDoneWithStop(PVPositionerSoftDone):
Expand All @@ -279,14 +252,3 @@ def stop(self, *, success=False):
self.setpoint.put(self.position)
time.sleep(2.0 / 60) # two clock ticks, allow for EPICS record processing
self.cb_readback() # re-evaluate soft done Signal


# -----------------------------------------------------------------------------
# :author: Pete R. Jemian
# :email: jemian@anl.gov
# :copyright: (c) 2017-2024, UChicago Argonne, LLC
#
# Distributed under the terms of the Argonne National Laboratory Open Source License.
#
# The full license is in the file LICENSE.txt, distributed with this software.
# -----------------------------------------------------------------------------
26 changes: 20 additions & 6 deletions apstools/devices/tests/test_aps_undulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,36 @@
from ophyd.sim import instantiate_fake_device

from ..aps_undulator import PlanarUndulator
from ..aps_undulator import Revolver_Undulator
from ..aps_undulator import STI_Undulator
from ..aps_undulator import Undulator2M
from ..aps_undulator import Undulator4M


@pytest.fixture()
def undulator():
undulator = instantiate_fake_device(PlanarUndulator, prefix="PSS:255ID:", name="undulator")
return undulator
TEST_PV_PREFIX = "TEST:PREFIX:"
TEST_CASES = [
[PlanarUndulator, TEST_PV_PREFIX],
[Revolver_Undulator, TEST_PV_PREFIX],
[STI_Undulator, TEST_PV_PREFIX],
[Undulator2M, TEST_PV_PREFIX],
[Undulator4M, TEST_PV_PREFIX],
]


def test_set_energy(undulator):
@pytest.mark.parametrize("klass, prefix", TEST_CASES)
def test_set_energy(klass, prefix):
undulator = instantiate_fake_device(klass, prefix=prefix, name="undulator")

assert undulator.start_button.get() == 0
undulator.energy.set(5)
assert undulator.energy.setpoint.get() == 5
assert undulator.start_button.get() == 1


def test_stop_energy(undulator):
@pytest.mark.parametrize("klass, prefix", TEST_CASES)
def test_stop_energy(klass, prefix):
undulator = instantiate_fake_device(klass, prefix=prefix, name="undulator")

assert undulator.stop_button.get() == 0
undulator.stop()
assert undulator.stop_button.get() == 1
2 changes: 1 addition & 1 deletion apstools/devices/tests/test_eurotherm_2216e.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def test_device():
assert not euro.connected

assert euro.tolerance.get() == 1
assert euro.update_target is False
assert euro.use_target is False
assert euro.target is None

cns = """
Expand Down
2 changes: 1 addition & 1 deletion apstools/devices/tests/test_positioner_soft_done.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def pos():
"""Test Positioner based on two analogout PVs."""
# fmt: off
pos = PVPositionerSoftDoneWithStop(
PV_PREFIX, readback_pv="float1", setpoint_pv="float2", name="pos"
PV_PREFIX, readback_pv="float1", setpoint_pv="float2", use_target=True, name="pos"
)
# fmt: on
pos.wait_for_connection()
Expand Down
2 changes: 1 addition & 1 deletion apstools/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
IOC_GP = "gp:" # general purpose IOC: motors, scalers, slits, ...
IOC_AD = "ad:" # ADSimDetector IOC

MASTER_TIMEOUT = 3
MASTER_TIMEOUT = 10
MAX_TESTING_RETRIES = 3
SHORT_DELAY_FOR_EPICS_IOC_DATABASE_PROCESSING = 2.0 / 60 # two 60Hz clock cycles

Expand Down
Loading