From 0adac207654f752a578d0cd1bed2b4a892b89c2e Mon Sep 17 00:00:00 2001 From: Gilberto Fabbris Date: Fri, 3 Sep 2021 18:08:36 -0500 Subject: [PATCH 01/12] ENH #530 add PVPositionerSoftDone --- apstools/devices.py | 113 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 112 insertions(+), 1 deletion(-) diff --git a/apstools/devices.py b/apstools/devices.py index 0019b1de1..bf4f6151e 100644 --- a/apstools/devices.py +++ b/apstools/devices.py @@ -104,7 +104,7 @@ from .synApps import * from ophyd import Component, Device, DeviceStatus, FormattedComponent -from ophyd import Signal, EpicsMotor, EpicsSignal, EpicsSignalRO +from ophyd import Signal, EpicsMotor, EpicsSignal, EpicsSignalRO, PVPositioner from ophyd.mca import EpicsMCARecord from ophyd.scaler import EpicsScaler, ScalerCH from ophyd.sim import SynSignalRO @@ -1620,6 +1620,117 @@ def calibrate_energy(self, value): self.use_set.put("Use") +class PVPositionerSoftDone(PVPositioner): + """ + PVPositioner that uses an internal "done" signal. + + Parameters + ---------- + prefix : str, optional + The device prefix used for all sub-positioners. This is optional as it + may be desirable to specify full PV names for PVPositioners. + readback_pv : str, optional + PV prefix of the readback signal. Disregarded if readback attribute is + created. + setpoint_pv : str, optional + PV prefix of the setpoint signal. Disregarded if setpoint attribute is + created. + tolerance : float, optional + Motion tolerance. The motion is considered done if + `abs(readback-setpoint) <= tolerance`. Defaults to `10^(-1*precision)`, + where `precision = setpoint.precision` + target_attr : str, optional + Used if the setpoint if incrementally controlled by EPICS (like with a + ramp). Then a target attribute signal must be defined, and its name + passed in this variable. + kwargs : + Passed to `ophyd.PVPositioner` + + Attributes + ---------- + setpoint : Signal + The setpoint (request) signal + readback : Signal or None + The readback PV (e.g., encoder position PV) + actuate : Signal or None + The actuation PV to set when movement is requested + actuate_value : any, optional + The actuation value, sent to the actuate signal when motion is + requested + stop_signal : Signal or None + The stop PV to set when motion should be stopped + stop_value : any, optional + The value sent to stop_signal when a stop is requested + """ + + # positioner + readback = FormattedComponent(EpicsSignalRO, "{prefix}{_readback_pv}", + kind="hinted", auto_monitor=True) + setpoint = FormattedComponent(EpicsSignal, "{prefix}{_setpoint_pv}", + kind="normal", put_complete=True) + done = Component(Signal, value=True, kind="config") + done_value = True + + tolerance = Component(Signal, value=-1, kind="config") + report_dmov_changes = Component(Signal, value=False, kind="omitted") + + @property + def precision(self): + return self.setpoint.precision + + def cb_readback(self, *args, **kwargs): + """ + Called when readback changes (EPICS CA monitor event). + """ + diff = self.readback.get() - self._target.get() + _tolerance = (self.tolerance.get() if self.tolerance.get() >= 0 else + 10**(-1*self.precision)) + dmov = abs(diff) <= _tolerance + if self.report_dmov_changes.get() and dmov != self.done.get(): + logger.debug(f"{self.name} reached: {dmov}") + self.done.put(dmov) + + def cb_setpoint(self, *args, **kwargs): + """ + Called when setpoint changes (EPICS CA monitor event). + When the setpoint is changed, force done=False. For any move, + done must go != done_value, then back to done_value (True). + Without this response, a small move (within tolerance) will not return. + Next update of readback will compute self.done. + """ + self.done.put(not self.done_value) + + def __init__(self, prefix="", *, readback_pv="", setpoint_pv="", + tolerance=None, target_attr="setpoint", **kwargs): + + self._setpoint_pv = setpoint_pv + self._readback_pv = readback_pv + + super().__init__(prefix=prefix, **kwargs) + + self._target = getattr(self, target_attr) + + # Make the default alias for the readback the name of the + # positioner itself as in EpicsMotor. + self.readback.name = self.name + + self.readback.subscribe(self.cb_readback) + self.setpoint.subscribe(self.cb_setpoint) + if tolerance: + self.tolerance.put(tolerance) + + def _setup_move(self, position): + '''Move and do not wait until motion is complete (asynchronous)''' + self.log.debug('%s.setpoint = %s', self.name, position) + self._target.put(position, wait=True) + if self._target != self.setpoint: + self.setpoint.put(position, wait=True) + 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) + self.cb_readback() # This is needed to force the first check. + + class ProcessController(Device): """ DEPRECATED (1.5.1): Use ``ophyd.PVPositioner`` instead. From c79795f6d102ad6ed088580725bfdd2774e627ee Mon Sep 17 00:00:00 2001 From: Pete R Jemian Date: Mon, 20 Sep 2021 13:15:52 -0500 Subject: [PATCH 02/12] DOCS #531 reST syntax --- apstools/devices.py | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/apstools/devices.py b/apstools/devices.py index bf4f6151e..74cc956f3 100644 --- a/apstools/devices.py +++ b/apstools/devices.py @@ -1622,32 +1622,36 @@ def calibrate_energy(self, value): class PVPositionerSoftDone(PVPositioner): """ - PVPositioner that uses an internal "done" signal. + PVPositioner that computes ``done`` as a soft signal. + + PARAMETERS - Parameters - ---------- prefix : str, optional - The device prefix used for all sub-positioners. This is optional as it + The device prefix used for all sub-positioners. + This is optional as it may be desirable to specify full PV names for PVPositioners. readback_pv : str, optional - PV prefix of the readback signal. Disregarded if readback attribute is - created. + PV prefix of the readback signal. + Disregarded if readback attribute is created. setpoint_pv : str, optional - PV prefix of the setpoint signal. Disregarded if setpoint attribute is - created. + PV prefix of the setpoint signal. + Disregarded if setpoint attribute is created. tolerance : float, optional - Motion tolerance. The motion is considered done if - `abs(readback-setpoint) <= tolerance`. Defaults to `10^(-1*precision)`, - where `precision = setpoint.precision` + Motion tolerance. The motion is considered *done* when:: + + abs(readback-setpoint) <= tolerance + + Defaults to ``10^(-1*precision)``, + where ``precision = setpoint.precision``. target_attr : str, optional - Used if the setpoint if incrementally controlled by EPICS (like with a - ramp). Then a target attribute signal must be defined, and its name - passed in this variable. + Used if the setpoint is controlled incrementally by EPICS + (like with a ramp). Then a target attribute signal must be + defined, and its name passed in this variable. kwargs : Passed to `ophyd.PVPositioner` - Attributes - ---------- + ATTRIBUTES + setpoint : Signal The setpoint (request) signal readback : Signal or None From b4884369729ea4161ce7c7293f3cc71b983a642d Mon Sep 17 00:00:00 2001 From: Pete R Jemian Date: Mon, 20 Sep 2021 13:18:10 -0500 Subject: [PATCH 03/12] STY #530 flake8 --- apstools/devices.py | 12 ++++++------ apstools/utils.py | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apstools/devices.py b/apstools/devices.py index 74cc956f3..acb4557e5 100644 --- a/apstools/devices.py +++ b/apstools/devices.py @@ -1627,24 +1627,24 @@ class PVPositionerSoftDone(PVPositioner): PARAMETERS prefix : str, optional - The device prefix used for all sub-positioners. + The device prefix used for all sub-positioners. This is optional as it may be desirable to specify full PV names for PVPositioners. readback_pv : str, optional - PV prefix of the readback signal. + PV prefix of the readback signal. Disregarded if readback attribute is created. setpoint_pv : str, optional - PV prefix of the setpoint signal. + PV prefix of the setpoint signal. Disregarded if setpoint attribute is created. tolerance : float, optional Motion tolerance. The motion is considered *done* when:: - + abs(readback-setpoint) <= tolerance - + Defaults to ``10^(-1*precision)``, where ``precision = setpoint.precision``. target_attr : str, optional - Used if the setpoint is controlled incrementally by EPICS + Used if the setpoint is controlled incrementally by EPICS (like with a ramp). Then a target attribute signal must be defined, and its name passed in this variable. kwargs : diff --git a/apstools/utils.py b/apstools/utils.py index 705813336..cc1bfeedc 100644 --- a/apstools/utils.py +++ b/apstools/utils.py @@ -422,9 +422,9 @@ def db_query(db, query): Bluesky database, an instance of ``databroker.catalog`` satisfying the ``query`` parameters. - See also - -------- - :func:`databroker.catalog.search` + .. seealso:: + + :func:`databroker.catalog.search` """ if query is None: From ccfbd77f26a5b58c520c98cc75d4377c081676e3 Mon Sep 17 00:00:00 2001 From: Pete R Jemian Date: Mon, 20 Sep 2021 13:22:06 -0500 Subject: [PATCH 04/12] DOC #530 --- apstools/devices.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apstools/devices.py b/apstools/devices.py index acb4557e5..9e976e220 100644 --- a/apstools/devices.py +++ b/apstools/devices.py @@ -44,6 +44,7 @@ ~EpicsMotorServoMixin ~EpicsMotorShutter ~EpicsOnOffShutter + ~PVPositionerSoftDone SHUTTERS @@ -1665,6 +1666,8 @@ class PVPositionerSoftDone(PVPositioner): The stop PV to set when motion should be stopped stop_value : any, optional The value sent to stop_signal when a stop is requested + + (new in apstools 1.5.2) """ # positioner From 6f3b76e5303025a7e7aa374af1aef8ac04b20615 Mon Sep 17 00:00:00 2001 From: Pete R Jemian Date: Mon, 20 Sep 2021 13:39:31 -0500 Subject: [PATCH 05/12] MNT #530 refactor into sub-package --- apstools/_devices/__init__.py | 1 + apstools/_devices/positioner_soft_done.py | 131 ++++++++++++++++++++++ apstools/devices.py | 118 +------------------ docs/source/source/_devices.rst | 3 + 4 files changed, 136 insertions(+), 117 deletions(-) create mode 100644 apstools/_devices/__init__.py create mode 100644 apstools/_devices/positioner_soft_done.py diff --git a/apstools/_devices/__init__.py b/apstools/_devices/__init__.py new file mode 100644 index 000000000..fda04da30 --- /dev/null +++ b/apstools/_devices/__init__.py @@ -0,0 +1 @@ +from .positioner_soft_done import PVPositionerSoftDone diff --git a/apstools/_devices/positioner_soft_done.py b/apstools/_devices/positioner_soft_done.py new file mode 100644 index 000000000..5c38a1e9a --- /dev/null +++ b/apstools/_devices/positioner_soft_done.py @@ -0,0 +1,131 @@ +""" +PVPositioner that computes ``done`` as a soft signal. +""" + +from ophyd import Component +from ophyd import FormattedComponent +from ophyd import PVPositioner +from ophyd import Signal +from ophyd import EpicsSignal +from ophyd import EpicsSignalRO +import logging + + +logger = logging.getLogger(__name__) + + +class PVPositionerSoftDone(PVPositioner): + """ + PVPositioner that computes ``done`` as a soft signal. + + PARAMETERS + + prefix : str, optional + The device prefix used for all sub-positioners. + This is optional as it + may be desirable to specify full PV names for PVPositioners. + readback_pv : str, optional + PV prefix of the readback signal. + Disregarded if readback attribute is created. + setpoint_pv : str, optional + PV prefix of the setpoint signal. + Disregarded if setpoint attribute is created. + tolerance : float, optional + Motion tolerance. The motion is considered *done* when:: + + abs(readback-setpoint) <= tolerance + + Defaults to ``10^(-1*precision)``, + where ``precision = setpoint.precision``. + target_attr : str, optional + Used if the setpoint is controlled incrementally by EPICS + (like with a ramp). Then a target attribute signal must be + defined, and its name passed in this variable. + kwargs : + Passed to `ophyd.PVPositioner` + + ATTRIBUTES + + setpoint : Signal + The setpoint (request) signal + readback : Signal or None + The readback PV (e.g., encoder position PV) + actuate : Signal or None + The actuation PV to set when movement is requested + actuate_value : any, optional + The actuation value, sent to the actuate signal when motion is + requested + stop_signal : Signal or None + The stop PV to set when motion should be stopped + stop_value : any, optional + The value sent to stop_signal when a stop is requested + + (new in apstools 1.5.2) + """ + + # positioner + readback = FormattedComponent(EpicsSignalRO, "{prefix}{_readback_pv}", + kind="hinted", auto_monitor=True) + setpoint = FormattedComponent(EpicsSignal, "{prefix}{_setpoint_pv}", + kind="normal", put_complete=True) + done = Component(Signal, value=True, kind="config") + done_value = True + + tolerance = Component(Signal, value=-1, kind="config") + report_dmov_changes = Component(Signal, value=False, kind="omitted") + + @property + def precision(self): + return self.setpoint.precision + + def cb_readback(self, *args, **kwargs): + """ + Called when readback changes (EPICS CA monitor event). + """ + diff = self.readback.get() - self._target.get() + _tolerance = (self.tolerance.get() if self.tolerance.get() >= 0 else + 10**(-1*self.precision)) + dmov = abs(diff) <= _tolerance + if self.report_dmov_changes.get() and dmov != self.done.get(): + logger.debug(f"{self.name} reached: {dmov}") + self.done.put(dmov) + + def cb_setpoint(self, *args, **kwargs): + """ + Called when setpoint changes (EPICS CA monitor event). + When the setpoint is changed, force done=False. For any move, + done must go != done_value, then back to done_value (True). + Without this response, a small move (within tolerance) will not return. + Next update of readback will compute self.done. + """ + self.done.put(not self.done_value) + + def __init__(self, prefix="", *, readback_pv="", setpoint_pv="", + tolerance=None, target_attr="setpoint", **kwargs): + + self._setpoint_pv = setpoint_pv + self._readback_pv = readback_pv + + super().__init__(prefix=prefix, **kwargs) + + self._target = getattr(self, target_attr) + + # Make the default alias for the readback the name of the + # positioner itself as in EpicsMotor. + self.readback.name = self.name + + self.readback.subscribe(self.cb_readback) + self.setpoint.subscribe(self.cb_setpoint) + if tolerance: + self.tolerance.put(tolerance) + + def _setup_move(self, position): + '''Move and do not wait until motion is complete (asynchronous)''' + self.log.debug('%s.setpoint = %s', self.name, position) + self._target.put(position, wait=True) + if self._target != self.setpoint: + self.setpoint.put(position, wait=True) + 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) + self.cb_readback() # This is needed to force the first check. diff --git a/apstools/devices.py b/apstools/devices.py index 9e976e220..eede31464 100644 --- a/apstools/devices.py +++ b/apstools/devices.py @@ -102,6 +102,7 @@ import time import warnings +from ._devices import PVPositionerSoftDone from .synApps import * from ophyd import Component, Device, DeviceStatus, FormattedComponent @@ -1621,123 +1622,6 @@ def calibrate_energy(self, value): self.use_set.put("Use") -class PVPositionerSoftDone(PVPositioner): - """ - PVPositioner that computes ``done`` as a soft signal. - - PARAMETERS - - prefix : str, optional - The device prefix used for all sub-positioners. - This is optional as it - may be desirable to specify full PV names for PVPositioners. - readback_pv : str, optional - PV prefix of the readback signal. - Disregarded if readback attribute is created. - setpoint_pv : str, optional - PV prefix of the setpoint signal. - Disregarded if setpoint attribute is created. - tolerance : float, optional - Motion tolerance. The motion is considered *done* when:: - - abs(readback-setpoint) <= tolerance - - Defaults to ``10^(-1*precision)``, - where ``precision = setpoint.precision``. - target_attr : str, optional - Used if the setpoint is controlled incrementally by EPICS - (like with a ramp). Then a target attribute signal must be - defined, and its name passed in this variable. - kwargs : - Passed to `ophyd.PVPositioner` - - ATTRIBUTES - - setpoint : Signal - The setpoint (request) signal - readback : Signal or None - The readback PV (e.g., encoder position PV) - actuate : Signal or None - The actuation PV to set when movement is requested - actuate_value : any, optional - The actuation value, sent to the actuate signal when motion is - requested - stop_signal : Signal or None - The stop PV to set when motion should be stopped - stop_value : any, optional - The value sent to stop_signal when a stop is requested - - (new in apstools 1.5.2) - """ - - # positioner - readback = FormattedComponent(EpicsSignalRO, "{prefix}{_readback_pv}", - kind="hinted", auto_monitor=True) - setpoint = FormattedComponent(EpicsSignal, "{prefix}{_setpoint_pv}", - kind="normal", put_complete=True) - done = Component(Signal, value=True, kind="config") - done_value = True - - tolerance = Component(Signal, value=-1, kind="config") - report_dmov_changes = Component(Signal, value=False, kind="omitted") - - @property - def precision(self): - return self.setpoint.precision - - def cb_readback(self, *args, **kwargs): - """ - Called when readback changes (EPICS CA monitor event). - """ - diff = self.readback.get() - self._target.get() - _tolerance = (self.tolerance.get() if self.tolerance.get() >= 0 else - 10**(-1*self.precision)) - dmov = abs(diff) <= _tolerance - if self.report_dmov_changes.get() and dmov != self.done.get(): - logger.debug(f"{self.name} reached: {dmov}") - self.done.put(dmov) - - def cb_setpoint(self, *args, **kwargs): - """ - Called when setpoint changes (EPICS CA monitor event). - When the setpoint is changed, force done=False. For any move, - done must go != done_value, then back to done_value (True). - Without this response, a small move (within tolerance) will not return. - Next update of readback will compute self.done. - """ - self.done.put(not self.done_value) - - def __init__(self, prefix="", *, readback_pv="", setpoint_pv="", - tolerance=None, target_attr="setpoint", **kwargs): - - self._setpoint_pv = setpoint_pv - self._readback_pv = readback_pv - - super().__init__(prefix=prefix, **kwargs) - - self._target = getattr(self, target_attr) - - # Make the default alias for the readback the name of the - # positioner itself as in EpicsMotor. - self.readback.name = self.name - - self.readback.subscribe(self.cb_readback) - self.setpoint.subscribe(self.cb_setpoint) - if tolerance: - self.tolerance.put(tolerance) - - def _setup_move(self, position): - '''Move and do not wait until motion is complete (asynchronous)''' - self.log.debug('%s.setpoint = %s', self.name, position) - self._target.put(position, wait=True) - if self._target != self.setpoint: - self.setpoint.put(position, wait=True) - 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) - self.cb_readback() # This is needed to force the first check. - - class ProcessController(Device): """ DEPRECATED (1.5.1): Use ``ophyd.PVPositioner`` instead. diff --git a/docs/source/source/_devices.rst b/docs/source/source/_devices.rst index 0f9687dfb..dfed86460 100644 --- a/docs/source/source/_devices.rst +++ b/docs/source/source/_devices.rst @@ -8,3 +8,6 @@ items described here. .. automodule:: apstools.devices :members: + +.. automodule:: apstools._devices.positioner_soft_done + :members: From 34bc1a457e9d80708395b65e0e4aa93eed73b0b4 Mon Sep 17 00:00:00 2001 From: Pete R Jemian Date: Mon, 20 Sep 2021 13:41:05 -0500 Subject: [PATCH 06/12] SYT #530 apply black code formatting --- apstools/_devices/positioner_soft_done.py | 35 ++++++++++++++++------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/apstools/_devices/positioner_soft_done.py b/apstools/_devices/positioner_soft_done.py index 5c38a1e9a..3cb6b50cb 100644 --- a/apstools/_devices/positioner_soft_done.py +++ b/apstools/_devices/positioner_soft_done.py @@ -64,10 +64,12 @@ class PVPositionerSoftDone(PVPositioner): """ # positioner - readback = FormattedComponent(EpicsSignalRO, "{prefix}{_readback_pv}", - kind="hinted", auto_monitor=True) - setpoint = FormattedComponent(EpicsSignal, "{prefix}{_setpoint_pv}", - kind="normal", put_complete=True) + readback = FormattedComponent( + EpicsSignalRO, "{prefix}{_readback_pv}", kind="hinted", auto_monitor=True + ) + setpoint = FormattedComponent( + EpicsSignal, "{prefix}{_setpoint_pv}", kind="normal", put_complete=True + ) done = Component(Signal, value=True, kind="config") done_value = True @@ -83,8 +85,11 @@ def cb_readback(self, *args, **kwargs): Called when readback changes (EPICS CA monitor event). """ diff = self.readback.get() - self._target.get() - _tolerance = (self.tolerance.get() if self.tolerance.get() >= 0 else - 10**(-1*self.precision)) + _tolerance = ( + self.tolerance.get() + if self.tolerance.get() >= 0 + else 10 ** (-1 * self.precision) + ) dmov = abs(diff) <= _tolerance if self.report_dmov_changes.get() and dmov != self.done.get(): logger.debug(f"{self.name} reached: {dmov}") @@ -100,8 +105,16 @@ def cb_setpoint(self, *args, **kwargs): """ self.done.put(not self.done_value) - def __init__(self, prefix="", *, readback_pv="", setpoint_pv="", - tolerance=None, target_attr="setpoint", **kwargs): + def __init__( + self, + prefix="", + *, + readback_pv="", + setpoint_pv="", + tolerance=None, + target_attr="setpoint", + **kwargs, + ): self._setpoint_pv = setpoint_pv self._readback_pv = readback_pv @@ -120,12 +133,12 @@ def __init__(self, prefix="", *, readback_pv="", setpoint_pv="", self.tolerance.put(tolerance) def _setup_move(self, position): - '''Move and do not wait until motion is complete (asynchronous)''' - self.log.debug('%s.setpoint = %s', self.name, position) + """Move and do not wait until motion is complete (asynchronous)""" + self.log.debug("%s.setpoint = %s", self.name, position) self._target.put(position, wait=True) if self._target != self.setpoint: self.setpoint.put(position, wait=True) if self.actuate is not None: - self.log.debug('%s.actuate = %s', self.name, self.actuate_value) + self.log.debug("%s.actuate = %s", self.name, self.actuate_value) self.actuate.put(self.actuate_value, wait=False) self.cb_readback() # This is needed to force the first check. From 486e7bd0a8c120866d62e0c6410908a44928f3aa Mon Sep 17 00:00:00 2001 From: Pete R Jemian Date: Mon, 20 Sep 2021 13:56:21 -0500 Subject: [PATCH 07/12] DOC #530 --- apstools/devices.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apstools/devices.py b/apstools/devices.py index eede31464..9ce11005b 100644 --- a/apstools/devices.py +++ b/apstools/devices.py @@ -44,7 +44,7 @@ ~EpicsMotorServoMixin ~EpicsMotorShutter ~EpicsOnOffShutter - ~PVPositionerSoftDone + ~apstools._devices.positioner_soft_done.PVPositionerSoftDone SHUTTERS From e58ebdf4a6ad40adb79ec10feb50ea42653cf350 Mon Sep 17 00:00:00 2001 From: Pete R Jemian Date: Mon, 20 Sep 2021 15:35:57 -0500 Subject: [PATCH 08/12] MNT #530 remove target_attr, keep target signal --- apstools/_devices/positioner_soft_done.py | 30 ++++++++++++++--------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/apstools/_devices/positioner_soft_done.py b/apstools/_devices/positioner_soft_done.py index 3cb6b50cb..9786a3c9e 100644 --- a/apstools/_devices/positioner_soft_done.py +++ b/apstools/_devices/positioner_soft_done.py @@ -8,6 +8,7 @@ from ophyd import Signal from ophyd import EpicsSignal from ophyd import EpicsSignalRO +from ophyd.signal import EpicsSignalBase import logging @@ -37,10 +38,6 @@ class PVPositionerSoftDone(PVPositioner): Defaults to ``10^(-1*precision)``, where ``precision = setpoint.precision``. - target_attr : str, optional - Used if the setpoint is controlled incrementally by EPICS - (like with a ramp). Then a target attribute signal must be - defined, and its name passed in this variable. kwargs : Passed to `ophyd.PVPositioner` @@ -59,6 +56,16 @@ class PVPositionerSoftDone(PVPositioner): The stop PV to set when motion should be stopped stop_value : any, optional The value sent to stop_signal when a stop is requested + target : Signal + The target value of a move request. + + In some controllers (such as temperature controllers), + the setpoint may be changed incrementally + towards this target value (such as a ramp or controlled trajectory). + In such cases, the ``target`` will be final value while ``setpoint`` + will be the current desired position. + + Otherwise, both ``setpoint`` and ``target`` will be set to the same value. (new in apstools 1.5.2) """ @@ -76,6 +83,8 @@ 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") + @property def precision(self): return self.setpoint.precision @@ -84,7 +93,7 @@ def cb_readback(self, *args, **kwargs): """ Called when readback changes (EPICS CA monitor event). """ - diff = self.readback.get() - self._target.get() + diff = self.readback.get() - self.setpoint.get() _tolerance = ( self.tolerance.get() if self.tolerance.get() >= 0 @@ -112,7 +121,6 @@ def __init__( readback_pv="", setpoint_pv="", tolerance=None, - target_attr="setpoint", **kwargs, ): @@ -121,8 +129,6 @@ def __init__( super().__init__(prefix=prefix, **kwargs) - self._target = getattr(self, target_attr) - # Make the default alias for the readback the name of the # positioner itself as in EpicsMotor. self.readback.name = self.name @@ -135,9 +141,11 @@ def __init__( def _setup_move(self, position): """Move and do not wait until motion is complete (asynchronous)""" self.log.debug("%s.setpoint = %s", self.name, position) - self._target.put(position, wait=True) - if self._target != self.setpoint: - self.setpoint.put(position, wait=True) + 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) 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) From 0241ba82a38f2ea82fba9fdb0b4b1bb37727d561 Mon Sep 17 00:00:00 2001 From: Pete R Jemian Date: Mon, 20 Sep 2021 15:44:31 -0500 Subject: [PATCH 09/12] DOC #535 --- apstools/_devices/positioner_soft_done.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apstools/_devices/positioner_soft_done.py b/apstools/_devices/positioner_soft_done.py index 9786a3c9e..dfda557e2 100644 --- a/apstools/_devices/positioner_soft_done.py +++ b/apstools/_devices/positioner_soft_done.py @@ -59,6 +59,8 @@ class PVPositionerSoftDone(PVPositioner): target : Signal The target value of a move request. + Override (in subclass) with `EpicsSignal` to connect with a PV. + In some controllers (such as temperature controllers), the setpoint may be changed incrementally towards this target value (such as a ramp or controlled trajectory). From c760d6ff609b3f74d90715d419e488817dc86aae Mon Sep 17 00:00:00 2001 From: Pete R Jemian Date: Tue, 5 Oct 2021 11:44:03 -0500 Subject: [PATCH 10/12] MNT #530 Should this class update target value? --- apstools/_devices/positioner_soft_done.py | 38 +++++++++++++++-------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/apstools/_devices/positioner_soft_done.py b/apstools/_devices/positioner_soft_done.py index dfda557e2..e135d8334 100644 --- a/apstools/_devices/positioner_soft_done.py +++ b/apstools/_devices/positioner_soft_done.py @@ -61,15 +61,18 @@ class PVPositionerSoftDone(PVPositioner): Override (in subclass) with `EpicsSignal` to connect with a PV. - In some controllers (such as temperature controllers), - the setpoint may be changed incrementally - towards this target value (such as a ramp or controlled trajectory). - In such cases, the ``target`` will be final value while ``setpoint`` - will be the current desired position. + In some controllers (such as temperature controllers), the setpoint + may be changed incrementally towards this target value (such as a + ramp or controlled trajectory). In such cases, the ``target`` will + be final value while ``setpoint`` will be the current desired position. Otherwise, both ``setpoint`` and ``target`` will be set to the same value. + update_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``. - (new in apstools 1.5.2) + (new in apstools 1.5.3) """ # positioner @@ -94,6 +97,10 @@ def precision(self): def cb_readback(self, *args, **kwargs): """ Called when readback changes (EPICS CA monitor event). + + Computes if the positioner is done moving:: + + done = |readback - setpoint| <= tolerance """ diff = self.readback.get() - self.setpoint.get() _tolerance = ( @@ -109,10 +116,12 @@ def cb_readback(self, *args, **kwargs): def cb_setpoint(self, *args, **kwargs): """ Called when setpoint changes (EPICS CA monitor event). - When the setpoint is changed, force done=False. For any move, - done must go != done_value, then back to done_value (True). + + 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. - Next update of readback will compute self.done. + Next update of readback will compute ``self.done``. """ self.done.put(not self.done_value) @@ -123,6 +132,7 @@ def __init__( readback_pv="", setpoint_pv="", tolerance=None, + update_target=True, **kwargs, ): @@ -134,6 +144,7 @@ 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.readback.subscribe(self.cb_readback) self.setpoint.subscribe(self.cb_setpoint) @@ -143,10 +154,11 @@ def __init__( def _setup_move(self, position): """Move and do not wait until motion is complete (asynchronous)""" self.log.debug("%s.setpoint = %s", self.name, position) - kwargs = {} - if issubclass(self.target.__class__, EpicsSignalBase): - kwargs["wait"] = True # Signal.put() warns if kwargs are given - self.target.put(position, **kwargs) + 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) if self.actuate is not None: self.log.debug("%s.actuate = %s", self.name, self.actuate_value) From a84b0ea85b28e85565593a49910362cd30dbe312 Mon Sep 17 00:00:00 2001 From: Pete R Jemian Date: Tue, 5 Oct 2021 11:58:39 -0500 Subject: [PATCH 11/12] DOC #535 --- apstools/_devices/positioner_soft_done.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/apstools/_devices/positioner_soft_done.py b/apstools/_devices/positioner_soft_done.py index e135d8334..0146dc2cb 100644 --- a/apstools/_devices/positioner_soft_done.py +++ b/apstools/_devices/positioner_soft_done.py @@ -38,6 +38,11 @@ class PVPositionerSoftDone(PVPositioner): Defaults to ``10^(-1*precision)``, where ``precision = setpoint.precision``. + update_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``. kwargs : Passed to `ophyd.PVPositioner` @@ -67,10 +72,6 @@ class PVPositionerSoftDone(PVPositioner): be final value while ``setpoint`` will be the current desired position. Otherwise, both ``setpoint`` and ``target`` will be set to the same value. - update_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``. (new in apstools 1.5.3) """ From 6bc118d9ded3af70a2ff1ab90b3ad2c35fa70b2a Mon Sep 17 00:00:00 2001 From: Pete R Jemian Date: Fri, 8 Oct 2021 15:56:01 -0500 Subject: [PATCH 12/12] MNT #549 raise ValueError if equal --- apstools/_devices/positioner_soft_done.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apstools/_devices/positioner_soft_done.py b/apstools/_devices/positioner_soft_done.py index 0146dc2cb..99079a5d8 100644 --- a/apstools/_devices/positioner_soft_done.py +++ b/apstools/_devices/positioner_soft_done.py @@ -137,6 +137,12 @@ def __init__( **kwargs, ): + if setpoint_pv == readback_pv: + raise ValueError( + f"readback_pv ({readback_pv})" + f" and setpoint_pv ({setpoint_pv})" + " must have different values" + ) self._setpoint_pv = setpoint_pv self._readback_pv = readback_pv