Skip to content

Commit

Permalink
Merge f9c85e7 into a8fdf33
Browse files Browse the repository at this point in the history
  • Loading branch information
prjemian committed May 26, 2019
2 parents a8fdf33 + f9c85e7 commit bcbfc6b
Showing 1 changed file with 140 additions and 1 deletion.
141 changes: 140 additions & 1 deletion apstools/devices.py
Expand Up @@ -74,6 +74,7 @@
~DualPf4FilterBox
~EpicsDescriptionMixin
~KohzuSeqCtl_Monochromator
~ProcessController
~Struck3820
Internal routines
Expand Down Expand Up @@ -121,7 +122,7 @@

logger = logging.getLogger(__name__)

"""for convenience""" # TODO: contribute to ophyd?
"""for convenience""" # TODO: contribute to ophyd?
SCALER_AUTOCOUNT_MODE = 1


Expand Down Expand Up @@ -1322,6 +1323,144 @@ class KohzuSeqCtl_Monochromator(Device):
crystal_type = Component(EpicsSignal, "BraggTypeMO")


class ProcessController(Device):
"""
common parts of a process controller support
A process controller keeps a signal (a readback value such as
temperature, vacuum, himdity, etc.) as close as possible
to a target (set point) value. It has additional fields
that describe parameters specific to the controller such
as PID loop, on/off, applied controller power, and other
details.
This is a base class to standardize the few common terms
used to command and record the target and readback values
of a process controller.
Subclasses should redefine (override) `controller_name`,
``signal``, ``target``, and ``units`` such as the example below.
Also set values for ``tolerance``, ``report_interval_s``, and
``poll_s`` suitable for the specific controller used.
*Floats*: ``signal``, ``target`', and ``tolerance`` will be
considered as floating point numbers in the code.
It is assumed in "meth"`settled()` that: ``|signal - target| <= tolerance``.
Override this *property* method if a different decision is needed.
EXAMPLE::
class MyLinkam(ProcessController):
controller_name = "MyLinkam Controller"
signal = Component(EpicsSignalRO, "temp")
target = Component(EpicsSignal, "setLimit", kind="omitted")
units = Component(Signal, kind="omitted", value="C")
controller = MyLinkam("my:linkam:", name="controller")
RE(controller.wait_until_settled(timeout=10))
controller.record_signal()
print(f"{controller.controller_name} settled? {controller.settled}")
def rampUp_rampDown():
'''ramp temperature up, then back down'''
yield from controller.set_target(25, timeout=180)
controller.report_interval_s = 10 # change report interval to 10s
for i in range(10, 0, -1):
print(f"hold at {self.value:.2f}{self.units.value}, time remaining: {i}s")
yield from bps.sleep(1)
yield from controller.set_target(0, timeout=180)
RE(test_plan())
"""

controller_name = "ProcessController"
signal = Component(Signal) # override in subclass
target = Component(Signal, kind="omitted") # override in subclass
tolerance = Component(Signal, kind="omitted", value=1) # override in subclass
units = Component(Signal, kind="omitted", value="") # override in subclass

report_interval_s = 5 # time between reports during loop, s
poll_s = 0.02 # time to wait during polling loop, s

def record_signal(self):
"""write signal to the console"""
msg = f"{self.controller_name} signal: {self.value:.2f}{self.units.value}"
print(msg)
return msg

def set_target(self, target, wait=True, timeout=None, timeout_fail=False):
"""change controller to new signal set point"""
yield from bps.mv(self.target, target)

msg = f"Set {self.controller_name} target to {target:.2f}{self.units.value}"
print(msg)

if wait:
yield from self.wait_until_settled(
timeout=timeout,
timeout_fail=timeout_fail)

@property
def value(self):
"""shortcut to self.signal.value"""
return self.signal.value

@property
def settled(self):
"""Is signal close enough to target?"""
diff = abs(self.signal.get() - self.target.value)
return diff <= self.tolerance

def wait_until_settled(self, timeout=None, timeout_fail=False):
"""
plan: wait for controller signal to reach target within tolerance
"""
# see: https://stackoverflow.com/questions/2829329/catch-a-threads-exception-in-the-caller-thread-in-python
t0 = time.time()
_st = DeviceStatus(self.signal)

if self.settled:
# just in case signal already at target
_st._finished(success=True)
else:
started = False

def changing_cb(*args, **kwargs):
if started and self.settled:
_st._finished(success=True)

token = self.signal.subscribe(changing_cb)
started = True
report = 0
while not _st.done and not self.settled:
elapsed = time.time() - t0
if timeout is not None and elapsed > timeout:
_st._finished(success=self.settled)
msg = f"{self.controller_name} Timeout after {elapsed:.2f}s"
msg += f", target {self.target.value:.2f}{self.units.value}"
msg += f", now {self.signal.get():.2f}{self.units.value}"
print(msg)
if timeout_fail:
raise TimeoutError(msg)
continue
if elapsed >= report:
report += self.report_interval_s.value
msg = f"Waiting {elapsed:.1f}s"
msg += f" to reach {self.target.value:.2f}{self.units.value}"
msg += f", now {self.signal.get():.2f}{self.units.value}"
print(msg)
yield from bps.sleep(self.poll_s)

self.signal.unsubscribe(token)

self.record_signal()
elapsed = time.time() - t0
print(f"Total time: {elapsed:.3f}s, settled:{_st.success}")


class Struck3820(Device):
"""Struck/SIS 3820 Multi-Channel Scaler (as used by USAXS)"""
start_all = Component(EpicsSignal, "StartAll")
Expand Down

0 comments on commit bcbfc6b

Please sign in to comment.