.. _positioners:

Positioners
***********

PVPositioner
------------

For example, this code defines a CS700 temperature controller. A temperature
controller is a kind of positioner, from ophyd's point of view, where the
"position" is the temperature.

.. code-block:: python

    from ophyd import PVPositioner, EpicsSignal, EpicsSignalRO
    from ophyd import Component as Cpt

    # Define a new kind of device.

    class CS700TemperatureController(PVPositioner):
        setpoint = Cpt(EpicsSignal, 'T-SP')
        readback = Cpt(EpicsSignalRO, 'T-I')
        done = Cpt(EpicsSignalRO, 'Cmd-Busy')
        stop_signal = Cpt(EpicsSignal, 'Cmd-Cmd')

    # Create an instance of this new kind of device.

    prefix = 'XF:28IDC-ES:1{Env:01}'
    cs700 = CS700TemperatureController(prefix, name='cs700')

    # When the cs700 has reached the set-point temperature, the 'done' signal
    # flips to 0.
    cs700.done_value = 0


.. autoclass:: ophyd.pv_positioner.PVPositioner


PVPositionerComparator
----------------------

As an example, take a shutter that has a setpoint PV and a readback
PV. The setpoint PV takes only CLOSE_VALVE and OPEN_VALVE commands.
The readback PV goes through the states OPEN -> MOVING -> CLOSED ->
MOVING and back to OPEN. The ``done_comparator()`` method knows that
the movement is done when the ``readback`` argument is not the MOVING
state.

From Bluesky, this device can be moved like
any other motor.

.. code-block:: python

    from ophyd.pv_positioner import PVPositionerComparator
    from ophyd import Component, EpicsSignal, EpicsSignalRO

    from enum import IntEnum, unique

    from typing import Any

    @unique
    class SetpointCmd(IntEnum):
      CLOSE_VALVE = 1
      OPEN_VALVE = 2

    @unique
    class ReadbackStatus(IntEnum):
      UNKNOWN = 0
      INVALID = 1
      CLOSED = 2
      OPEN = 3
      MOVING = 4

    class Shutter(PVPositionerComparator):
      setpoint = Component(EpicsSignal, ":OPEN_CLOSE_CMD")
      readback = Component(EpicsSignalRO, ":OPEN_CLOSE_STATUS")

      def __init__(self, prefix: str, *, name: str, **kwargs):
	kwargs.update({"limits": (SetpointCmd.CLOSE_VALVE,
				  SetpointCmd.OPEN_VALVE)})
	super().__init__(prefix, name=name, **kwargs)

      def done_comparator(self, readback:Any, setpoint:Any) -> bool:
	return readback != ReadbackStatus.MOVING


.. autoclass:: ophyd.pv_positioner.PVPositionerComparator

.. autoclass:: ophyd.pv_positioner.PVPositionerIsClose

PseudoPositioner
----------------

An ophyd ``PseudoPositioner`` relates one or more pseudo (virtual) axes to one
or more real (physical) axes via forward and inverse calculations. To define
such a PseudoPositioner, one must subclass from PseudoPositioner:

.. code-block:: python

    from ophyd import (PseudoPositioner, PseudoSingle, EpicsMotor)
    from ophyd import (Component as Cpt)
    from ophyd.pseudopos import (pseudo_position_argument,
                                 real_position_argument)


    class Pseudo3x3(PseudoPositioner):
        # The pseudo positioner axes:
        px = Cpt(PseudoSingle, limits=(-10, 10))
        py = Cpt(PseudoSingle, limits=(-10, 10))
        pz = Cpt(PseudoSingle)

        # The real (or physical) positioners:
        rx = Cpt(EpicsMotor, 'XF:31IDA-OP{Tbl-Ax:X1}Mtr')
        ry = Cpt(EpicsMotor, 'XF:31IDA-OP{Tbl-Ax:X2}Mtr')
        rz = Cpt(EpicsMotor, 'XF:31IDA-OP{Tbl-Ax:X3}Mtr')

        @pseudo_position_argument
        def forward(self, pseudo_pos):
            '''Run a forward (pseudo -> real) calculation'''
            return self.RealPosition(rx=-pseudo_pos.px,
                                     ry=-pseudo_pos.py,
                                     rz=-pseudo_pos.pz)

        @real_position_argument
        def inverse(self, real_pos):
            '''Run an inverse (real -> pseudo) calculation'''
            return self.PseudoPosition(px=-real_pos.rx,
                                       py=-real_pos.ry,
                                       pz=-real_pos.rz)

``Pseudo3x3`` above is a pseudo positioner with 3 pseudo axes and 3 real axes.
The pseudo axes are defined in order as (px, py, pz). Similarly, the real
positioners are (rx, ry, rz).

There is no restriction that the real axes must be tied to physical hardware.
A physical axis could just as well be a ``SoftPositioner``, or any subclass of
``PositionerBase`` (with the sole exception of ``PseudoSingle``).

The forward calculation says that, for any given pseudo position, the real
motors should move to the opposite position. For example, for a pseudo position
of (px=1, py=2, pz=3), the corresponding real position would be (rx=-1, ry=-2,
rz=-3). The inverse calculation is similar, in going from a real position to a
pseudo position.

The two decorators ``@real_position_argument`` and
``@pseudo_position_argument`` are used here for convenience so that one can
call these functions in a variety of ways, all of which generate a correct
PseudoPosition tuple as the first argument to your calculation method.
Positions can be specified in the following ways:

* As positional arguments:

.. code-block:: python

    pseudo.forward(px, py, pz)


* As a sequence or PseudoPosition/RealPosition:

.. code-block:: python

    pseudo.forward((px, py, pz))
    pseudo.forward(pseudo.PseudoPosition(px, py, pz))


* As kwargs:

.. code-block:: python

    pseudo.forward(px=1, py=2, pz=3)


``move`` is decorated like this on PseudoPositioner, meaning you can also call
it with this syntax.

.. autoclass:: ophyd.pseudopos.PseudoSingle
.. autoclass:: ophyd.pseudopos.PseudoPositioner


SoftPositioner
--------------

A ``SoftPositioner`` is a positioner which has no corresponding physical motor.
On its own, it is most useful for debugging scanning logic when moving physical
motors is either undesirable or not possible.

Used as-is, a ``SoftPositioner`` will "move" to the requested position
immediately.

``PseudoSingle`` and ``PseudoPositioner``, for example, are implemented as
heavily customized ``SoftPositioner`` subclasses.

.. autoclass:: ophyd.positioner.PositionerBase
.. autoclass:: ophyd.positioner.SoftPositioner

.. code-block:: python

    from ophyd import SoftPositioner
    my_positioner = SoftPositioner(name='my_positioner')
