Skip to content

Commit

Permalink
Merge pull request #898 from BCDA-APS/897-temperature-sims
Browse files Browse the repository at this point in the history
Hoist simulated controller positioners from bluesky_training
  • Loading branch information
prjemian committed Dec 27, 2023
2 parents 35fc705 + cd9e1b0 commit 7d20d4c
Show file tree
Hide file tree
Showing 8 changed files with 273 additions and 21 deletions.
11 changes: 9 additions & 2 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,11 @@ release expected by 2023-12-15
New Features
------------

* Add (ophyd) device support for
* DG-645 digital delay/pulse generator
* Add (ophyd) device support for:
* DG-645 digital delay/pulse generator.
* Measurement Computing USB CTR08 High-Speed Counter/Timer
* Simulated process controller as positioner using EPICS swait record.
* Simulated process controller as positioner using EPICS transform record.
* Add subnet check for APSU beamlines.
* Add template support for writing NeXus/HDF5 files.
* New lineup2() plan can be used in console, notebooks, and queueserver.
Expand All @@ -46,6 +48,11 @@ Maintenance
* Move ``.OVAL`` field from ``EpicsRecordOutputFields to new ``EpicsRecordAnalogOutputFields``
* Write tables of plot statistics in most compact form.

Known Problems
--------------

* Remove ScalerMotorFlyer, pending issue #763.

1.6.17
******

Expand Down
18 changes: 11 additions & 7 deletions apstools/devices/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,14 @@

from .eurotherm_2216e import Eurotherm2216e

from .flyer_motor_scaler import FlyerBase
from .flyer_motor_scaler import ActionsFlyerBase
from .flyer_motor_scaler import ScalerMotorFlyer
from .flyer_motor_scaler import SignalValueStack
from .flyer_motor_scaler import _SMFlyer_Step_1
from .flyer_motor_scaler import _SMFlyer_Step_2
from .flyer_motor_scaler import _SMFlyer_Step_3
# issue #763
# from .flyer_motor_scaler import FlyerBase
# from .flyer_motor_scaler import ActionsFlyerBase
# from .flyer_motor_scaler import ScalerMotorFlyer
# from .flyer_motor_scaler import SignalValueStack
# from .flyer_motor_scaler import _SMFlyer_Step_1
# from .flyer_motor_scaler import _SMFlyer_Step_2
# from .flyer_motor_scaler import _SMFlyer_Step_3

from .kohzu_monochromator import KohzuSeqCtl_Monochromator

Expand Down Expand Up @@ -101,6 +102,9 @@
from .shutters import ShutterBase
from .shutters import SimulatedApsPssShutterWithStatus

from .simulated_controllers import SimulatedSwaitControllerPositioner
from .simulated_controllers import SimulatedTransformControllerPositioner

from .srs570_preamplifier import SRS570_PreAmplifier

from .struck3820 import Struck3820
Expand Down
File renamed without changes.
170 changes: 170 additions & 0 deletions apstools/devices/simulated_controllers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
"""
Simulate process controllers as positioners using EPICS records.
.. autosummary::
~SimulatedSwaitControllerPositioner
~SimulatedTransformControllerPositioner
"""

from . import PVPositionerSoftDoneWithStop
from ..synApps import SwaitRecord
from ..synApps import TransformRecord
from ophyd import FormattedComponent as FC
import time


class SimulatedSwaitControllerPositioner(PVPositionerSoftDoneWithStop):
"""
Simulated process controller as positioner with EPICS swait record.
The swait record completes the feedback loop, computing
the next simulated controller reading.
Example with ``swait`` record::
controller = SimulatedSwaitControllerPositioner(
"",
name="controller",
loop_pv="gp:userCalc1",
)
controller.wait_for_connection()
controller.setup(25)
.. autosummary::
~setup
"""

loop = FC(SwaitRecord, "{loop_pv}", kind="config")

def __init__(self, *args, loop_pv="", **kwargs):
if len(loop_pv.strip()) == 0:
raise ValueError("Must supply a value for 'loop_pv'.")

self.loop_pv = loop_pv

kwargs["readback_pv"] = f"{loop_pv}.VAL"
kwargs["setpoint_pv"] = f"{loop_pv}.B"

super().__init__(*args, **kwargs)

def setup(
self,
setpoint,
label="controller",
noise=1,
period="1 second",
max_change=1,
tolerance=1,
):
"""
Configure the swait record as a process controller.
"""
self.tolerance.put(tolerance)

swait = self.loop
swait.reset() # remove any prior configuration
time.sleep(2.0 / 60) # short pause for IOC processing

swait.description.put(label)
swait.channels.A.input_value.put(setpoint) # readback
swait.channels.A.input_pv.put(swait.calculated_value.pvname)
swait.channels.B.input_value.put(setpoint) # setpoint
swait.channels.C.input_value.put(noise)
swait.channels.D.input_value.put(max_change)
swait.scanning_rate.put(period)
swait.precision.put(3)
swait.calculated_value.put(setpoint) # preset initial value
swait.calculation.put("A+max(-D,min(D,(B-A)))+C*(RNDM-0.5)")


class SimulatedTransformControllerPositioner(PVPositionerSoftDoneWithStop):
"""
Simulated process controller as positioner with EPICS transform record.
The transform record completes the feedback loop, computing the next
simulated controller reading and reporting if the readback is "in position".
Example with ``transform`` record::
controller = SimulatedTransformControllerPositioner(
"", name="controller", loop_pv="gp:userTran1",
) controller.wait_for_connection() temperature.setup(25)
.. autosummary::
~setup
"""

loop = FC(TransformRecord, "{loop_pv}", kind="config")

def __init__(self, *args, loop_pv="", **kwargs):
if len(loop_pv.strip()) == 0:
raise ValueError("Must supply a value for 'loop_pv'.")

self.loop_pv = loop_pv

kwargs["readback_pv"] = f"{loop_pv}.H"
kwargs["setpoint_pv"] = f"{loop_pv}.B"

super().__init__(*args, **kwargs)

self.following_error = self.loop.channels.E.current_value

def setup(
self,
setpoint,
label="controller",
noise=2,
period="1 second",
max_change=2,
tolerance=1,
):
"""
Configure the transform record as a temperature controller.
"""
self.wait_for_connection()
self.tolerance.put(tolerance)

transform = self.loop
transform.reset() # remove any prior configuration
time.sleep(2.0 / 60) # short pause for IOC processing

transform.description.put(label)

transform.channels.A.comment.put("last readback")
transform.channels.A.current_value.put(setpoint) # readback
transform.channels.A.input_pv.put(transform.channels.H.current_value.pvname)

transform.channels.B.comment.put("setpoint")
transform.channels.B.current_value.put(setpoint) # setpoint

transform.channels.C.comment.put("noise level")
transform.channels.C.current_value.put(noise)

transform.channels.D.comment.put("max_change")
transform.channels.D.current_value.put(max_change)

transform.channels.E.comment.put("following error")
transform.channels.E.expression.put("B-A")

transform.channels.F.comment.put("step")
transform.channels.F.expression.put("max(-D,min(D,E))")

transform.channels.G.comment.put("noise")
transform.channels.G.expression.put("C*(RNDM-0.5)")

transform.channels.H.comment.put("readback")
transform.channels.H.current_value.put(setpoint) # preset initial value
transform.channels.H.expression.put("A+F+G")

transform.channels.I.comment.put("tolerance")
transform.channels.I.current_value.put(tolerance)

transform.channels.J.comment.put("in position")
transform.channels.J.expression.put("abs(H-B)<=I")

transform.precision.put(3)

transform.scanning_rate.put(period)
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# FIXME: refactor for new code
# FIXME: Issue #763, #899 : refactor for new code

import uuid

Expand Down Expand Up @@ -78,7 +78,9 @@ def test_bases(flyer_class):
[-1, 1, None, None, None, NUM_READING_KEYS, 10],
[-1, 1, 2, 2, 1, NUM_READING_KEYS, 2],
[-1, 1, 1, 2, 0.1, NUM_READING_KEYS, 10],
[0, 0, 1, 2, 0.1, NUM_READING_KEYS, 0],
# FIXME: #899 - no data
# ValueError: The pack_event_page() function was called with empty *args. Cannot create an EventPage from an empty collection of Events because the 'descriptor' field in an EventPage cannot be NULL.
# [0, 0, 1, 2, 0.1, NUM_READING_KEYS, 0],
[1, -1, 1, 2, 0.1, NUM_READING_KEYS, 10],
],
)
Expand Down
51 changes: 51 additions & 0 deletions apstools/devices/tests/test_simulated_controllers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""
Test the simulated controllers.
"""

import math
from contextlib import nullcontext as does_not_raise

import pytest

from ...tests import IOC_GP
from ...tests import timed_pause
from .. import simulated_controllers as stc

PV_SWAIT = f"{IOC_GP}userCalc7"
PV_TRANS = f"{IOC_GP}userTran7"


@pytest.mark.parametrize("sp", [-55, 120, 998])
@pytest.mark.parametrize(
"pv, controller_class, context, exp_info",
[
[PV_SWAIT, stc.SimulatedSwaitControllerPositioner, does_not_raise(), "None"],
[PV_TRANS, stc.SimulatedTransformControllerPositioner, does_not_raise(), "None"],
["", stc.SimulatedSwaitControllerPositioner, pytest.raises(ValueError), "Must supply a value for"],
["", stc.SimulatedTransformControllerPositioner, pytest.raises(ValueError), "Must supply a value for"],
["wrong_pv", stc.SimulatedSwaitControllerPositioner, pytest.raises(TimeoutError), "Failed to connect"],
["wrong_pv", stc.SimulatedTransformControllerPositioner, pytest.raises(TimeoutError), "Failed to connect"],
],
)
@pytest.mark.parametrize("tol", [0.99, 2, 5])
def test_simulators(sp, pv, controller_class, context, exp_info, tol):
"""
Test the simulator.
"""
with context as info:
sim = controller_class("", loop_pv=pv, name="sim")
sim.wait_for_connection()
assert exp_info in str(info), f"{str(info)=!r}"
if exp_info != "None":
return

timed_pause()
assert sim.connected

sim.setup(sp, tolerance=tol)
timed_pause()

assert math.isclose(sim.setpoint.get(), sp, abs_tol=0.01)
assert math.isclose(sim.tolerance.get(), tol, abs_tol=0.0001)
assert math.isclose(sim.position, sp, abs_tol=tol)
assert sim.inposition
38 changes: 28 additions & 10 deletions docs/source/api/_devices.rst
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,14 @@ Detector & Scaler Support
Fly Scan Support
+++++++++++++++++++++++

.. autosummary::
.. issue #763
.. autosummary::
~apstools.devices.flyer_motor_scaler.FlyerBase
~apstools.devices.flyer_motor_scaler.ActionsFlyerBase
~apstools.devices.flyer_motor_scaler.ScalerMotorFlyer
~apstools.devices.flyer_motor_scaler.FlyerBase
~apstools.devices.flyer_motor_scaler.ActionsFlyerBase
~apstools.devices.flyer_motor_scaler.ScalerMotorFlyer
``ScalerMotorFlyer()`` support withdrawn pending issue #763.

.. _devices.motors:

Expand All @@ -122,6 +125,8 @@ Motors, Positioners, Axes, ...
~apstools.devices.positioner_soft_done.PVPositionerSoftDoneWithStop
~apstools.devices.shutters.EpicsMotorShutter
~apstools.devices.shutters.EpicsOnOffShutter
~apstools.devices.simulated_controllers.SimulatedSwaitControllerPositioner
~apstools.devices.simulated_controllers.SimulatedTransformControllerPositioner

.. _devices.shutters:

Expand Down Expand Up @@ -175,6 +180,8 @@ Controllers
~apstools.devices.ptc10_controller.PTC10RtdChannel
~apstools.devices.ptc10_controller.PTC10TcChannel
~apstools.devices.ptc10_controller.PTC10PositionerMixin
~apstools.devices.simulated_controllers.SimulatedSwaitControllerPositioner
~apstools.devices.simulated_controllers.SimulatedTransformControllerPositioner

Readers
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down Expand Up @@ -204,11 +211,15 @@ Other Support
~apstools.devices.epics_scan_id_signal.EpicsScanIdSignal
~apstools.devices.measComp_usb_ctr_support.MeasCompCtr
~apstools.devices.kohzu_monochromator.KohzuSeqCtl_Monochromator
~apstools.devices.flyer_motor_scaler.SignalValueStack
~apstools.devices.simulated_controllers.SimulatedSwaitControllerPositioner
~apstools.devices.simulated_controllers.SimulatedTransformControllerPositioner
~apstools.devices.srs570_preamplifier.SRS570_PreAmplifier
~apstools.devices.struck3820.Struck3820
~apstools.devices.delay.DG645Delay

.. issue #763
~apstools.devices.flyer_motor_scaler.SignalValueStack
Internal Routines
+++++++++++++++++

Expand Down Expand Up @@ -293,11 +304,12 @@ All Submodules
:show-inheritance:
:inherited-members:

.. automodule:: apstools.devices.flyer_motor_scaler
:members:
:private-members:
:show-inheritance:
:inherited-members:
.. issue #763
.. automodule apstools.devices.flyer_motor_scaler
:members:
:private-members:
:show-inheritance:
:inherited-members:
.. automodule:: apstools.devices.kohzu_monochromator
:members:
Expand Down Expand Up @@ -371,6 +383,12 @@ All Submodules
:show-inheritance:
:inherited-members:

.. automodule:: apstools.devices.simulated_controllers
:members:
:private-members:
:show-inheritance:
:inherited-members:

.. automodule:: apstools.devices.srs570_preamplifier
:members:
:private-members:
Expand Down

0 comments on commit 7d20d4c

Please sign in to comment.