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

PVPositionerSoftDone should set done False at start of move #954

Merged
merged 36 commits into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
2fca548
MNT #916 force done False at start of move
prjemian Apr 4, 2024
ef5371a
MNT #810 comment pytest.mark.local
prjemian Apr 4, 2024
7093b19
TST #810 remove local test markers and re-run CI
prjemian Apr 4, 2024
43fc8b9
Merge branch 'main' into 916-PVPositionerSoftDone-update
prjemian Apr 4, 2024
5e6ed98
MNT #810 remove SP & RB counters not needed for operations
prjemian Apr 4, 2024
20c2fa5
MNT #810 remove unnecessary call
prjemian Apr 4, 2024
edeef53
DOC #916 update the release notes
prjemian Apr 4, 2024
c18d860
TST #810 adjust for observed CI failure
prjemian Apr 4, 2024
71a2531
TST #810 discard unnecessary test
prjemian Apr 4, 2024
570e411
Merge branch 'main' into 916-PVPositionerSoftDone-update
prjemian Apr 9, 2024
9e8a793
TST #810 not failing now
prjemian Apr 9, 2024
64a3c64
MNT #954 refactor setpoint into custom subclass
prjemian Apr 11, 2024
70a6dc1
TST #954 respond to refactor
prjemian Apr 11, 2024
3fa2150
TST #954 more tests to change since target=None is default now
prjemian Apr 11, 2024
10788f2
TST #954 stylistic change of test for None
prjemian Apr 11, 2024
162cb3d
MNT #954 TARGET_UNDEFINED, remove custom get method
prjemian Apr 11, 2024
68d0d3e
TST #954 TARGET_UNDEFINED symbol
prjemian Apr 11, 2024
9c26b8a
TST #954 rename class so does not start with "Test"
prjemian Apr 11, 2024
80e86e6
GIT #916 ignore dev_* markdown files
prjemian Jul 9, 2024
d001097
Merge branch 'main' into 916-PVPositionerSoftDone-update
prjemian Jul 9, 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
f6c366d
Merge branch 'main' into 916-PVPositionerSoftDone-update
prjemian Aug 26, 2024
791232a
retrigger checks
gfabbris Aug 26, 2024
afe2cdc
Merge branch 'pvpositioner' into 916-PVPositionerSoftDone-update
gfabbris Aug 26, 2024
78a76f5
re-insert the 3.8 unit testing, see https://github.com/BCDA-APS/apsto…
gfabbris Aug 28, 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
1 change: 1 addition & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ Fixes
* lineup2() should work with low intensity peaks.
* lineup2() would raise ZeroDivideError in some cases.
* Increase minimum aps-dm-api version to 8.
* PVPositionerSoftDone should set 'done' to False at start of a move.
* Race condition with SR570 pre-amp.

Maintenance
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
50 changes: 18 additions & 32 deletions apstools/devices/positioner_soft_done.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,17 @@
from ophyd import FormattedComponent
from ophyd import PVPositioner
from ophyd import Signal
from ophyd.signal import EpicsSignalBase

# from ..tests import timed_pause

logger = logging.getLogger(__name__)

# Must use a data type that can be serialized as json (Python's None cannot be serialized)
# This ValueError: Cannot determine the appropriate bluesky-friendly data type for value
# None of Python type <class 'NoneType'>. Supported types include: int, float, str, and
# iterables such as list, tuple, np.ndarray, and so on.
TARGET_UNDEFINED = "undefined"


class PVPositionerSoftDone(PVPositioner):
"""
Expand All @@ -48,11 +53,11 @@ class PVPositionerSoftDone(PVPositioner):

Defaults to ``10^(-1*precision)``,
where ``precision = setpoint.precision``.
update_target : bool
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 @@ -101,10 +106,7 @@ class PVPositionerSoftDone(PVPositioner):
tolerance = Component(Signal, value=-1, kind="config")
report_dmov_changes = Component(Signal, value=False, kind="omitted")

target = Component(Signal, value="None", kind="config")

_rb_count = 0
_sp_count = 0
target = Component(Signal, value=TARGET_UNDEFINED, kind="config")

def __init__(
self,
Expand All @@ -113,7 +115,7 @@ def __init__(
readback_pv="",
setpoint_pv="",
tolerance=None,
update_target=True,
use_target=False,
**kwargs,
):
# fmt: off
Expand All @@ -132,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 @@ -159,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 @@ -171,8 +178,6 @@ def cb_readback(self, *args, **kwargs):
if idle:
return

self._rb_count += 1

if self.inposition:
self.done.put(self.done_value)
if self.report_dmov_changes.get():
Expand All @@ -192,7 +197,6 @@ def cb_setpoint(self, *args, **kwargs):
and do not react to the "wrong" signature.
"""
if "value" in kwargs and "status" not in kwargs:
self._sp_count += 1
self.done.put(not self.done_value)
logger.debug("cb_setpoint: done=%s, setpoint=%s", self.done.get(), self.setpoint.get())

Expand All @@ -208,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 @@ -221,18 +225,11 @@ 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)
if self.update_target:
kwargs = {}
if issubclass(self.target.__class__, EpicsSignalBase):
kwargs["wait"] = True # Signal.put() warns if kwargs are given
self.target.put(position, **kwargs)
self.setpoint.put(position, wait=True)
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)
# This is needed because in a special case the setpoint.put does not
# run the "sub_value" subscriptions.
self.cb_setpoint()
self.cb_readback() # This is needed to force the first check.


Expand All @@ -255,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: [email protected]
# :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.
# -----------------------------------------------------------------------------
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
9 changes: 5 additions & 4 deletions apstools/devices/tests/test_lakeshores.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from ...tests import IOC_GP
from ..lakeshore_controllers import LakeShore336Device
from ..lakeshore_controllers import LakeShore340Device
from ..positioner_soft_done import TARGET_UNDEFINED

PV_PREFIX = f"phony:{IOC_GP}lakeshore:"

Expand All @@ -16,8 +17,8 @@ def test_lakeshore_336():
assert not t336.connected

# Signal components
assert t336.loop1.target.get() == "None"
assert t336.loop2.target.get() == "None"
assert t336.loop1.target.get() == TARGET_UNDEFINED
assert t336.loop2.target.get() == TARGET_UNDEFINED

assert t336.loop1.tolerance.get() == 0.1
assert t336.loop2.tolerance.get() == 0.1
Expand Down Expand Up @@ -68,8 +69,8 @@ def test_lakeshore_340():
assert not t340.connected

# Signal components
assert t340.control.target.get() == "None"
assert t340.sample.target.get() == "None"
assert t340.control.target.get() == TARGET_UNDEFINED
assert t340.sample.target.get() == TARGET_UNDEFINED

assert t340.control.tolerance.get() == 0.1
assert t340.sample.tolerance.get() == 0.1
Expand Down
31 changes: 12 additions & 19 deletions apstools/devices/tests/test_positioner_soft_done.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from ...utils import run_in_thread
from ..positioner_soft_done import PVPositionerSoftDone
from ..positioner_soft_done import PVPositionerSoftDoneWithStop
from ..positioner_soft_done import TARGET_UNDEFINED

PV_PREFIX = f"{IOC_GP}gp:"
delay_active = False
Expand All @@ -35,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 Expand Up @@ -93,9 +94,8 @@ def confirm_in_position(p, dt):
p.readback.get(use_monitor=False) # force a read from the IOC
p.cb_readback() # update self.done

# collect these values at one instant (as close as possible).
c_rb = p._rb_count # do not expect any new callbacks during this method
c_sp = p._sp_count
# Collect these values at one instant (as close in time as possible).
# Do not expect any new callbacks during this function.
dmov = p.done.get()
rb = p.readback.get()
sp = p.setpoint.get()
Expand All @@ -106,18 +106,13 @@ def confirm_in_position(p, dt):
f"{p.name=}"
f" {rb=:.5f} {sp=:.5f} {tol=}"
f" {dt=:.4f}s"
f" {p._sp_count=}"
f" {p._rb_count=}"
f" {p.done=}"
f" {p.done_value=}"
f" {time.time()=:.4f}"
)
# fmt: on

assert p._rb_count == c_rb, diagnostics
assert p._sp_count == c_sp, diagnostics
assert dmov == p.done_value, diagnostics
# assert math.isclose(rb, sp, abs_tol=tol), diagnostics


@run_in_thread
Expand Down Expand Up @@ -202,20 +197,17 @@ def test_structure(device, has_inposition):
assert pos.setpoint.pvname == "v"
assert pos.done.get() is True
assert pos.done_value is True
assert pos.target.get() == "None"
assert pos.target.get() == TARGET_UNDEFINED
assert pos.tolerance.get() == -1


@pytest.mark.local
def test_put_and_stop(rbv, prec, pos):
assert pos.tolerance.get() == -1
assert pos.precision == prec.get()

def motion(rb_initial, target, rb_mid=None):
rbv.put(rb_initial) # make the readback to different
c_sp = pos._sp_count
pos.setpoint.put(target)
assert pos._sp_count == c_sp + 1
assert math.isclose(pos.readback.get(use_monitor=False), rb_initial, abs_tol=0.02)
assert math.isclose(pos.setpoint.get(use_monitor=False), target, abs_tol=0.02)
assert pos.done.get() != pos.done_value
Expand Down Expand Up @@ -248,7 +240,6 @@ def motion(rb_initial, target, rb_mid=None):
motion(1, 0, 0.5) # interrupted move


@pytest.mark.local
def test_move_and_stop_nonzero(rbv, pos):
timed_pause()

Expand All @@ -271,9 +262,14 @@ def test_move_and_stop_nonzero(rbv, pos):
assert pos.inposition


@pytest.mark.local
def test_move_and_stopped_early(rbv, pos):
def motion(target, delay, interrupt=False):
"""
Test moving pos to target. Update rbv after delay.

If interrupt is True, stop the move before it is done
(at a time that is less than the 'delay' value).
"""
timed_pause(0.1) # allow previous activities to settle down

t0 = time.time()
Expand All @@ -286,8 +282,7 @@ def motion(target, delay, interrupt=False):
rb_new = pos.readback.get(use_monitor=False)
arrived = math.isclose(rb_new, target, abs_tol=pos.actual_tolerance)
# fmt: on
if interrupt:
assert not status.done
if interrupt and not status.done:
assert not status.success
assert not arrived, f"{dt=:.3f}"
pos.stop()
Expand Down Expand Up @@ -350,11 +345,9 @@ def test_position_sequence_calcpos(target, calcpos):
def motion(p, goal):
timed_pause(0.1) # allow previous activities to settle down

c_sp = p._sp_count
t0 = time.time()
status = p.move(goal)
dt = time.time() - t0
assert p._sp_count == c_sp + 1
assert status.elapsed > 0, str(status)
assert status.done, str(status)

Expand Down
Loading