Skip to content

Commit 7405131

Browse files
committed
MNT #156 generalize
1 parent 40687ad commit 7405131

File tree

1 file changed

+92
-70
lines changed

1 file changed

+92
-70
lines changed

apstools/devices.py

Lines changed: 92 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
~DualPf4FilterBox
7575
~EpicsDescriptionMixin
7676
~KohzuSeqCtl_Monochromator
77+
~ProcessController
7778
~Struck3820
7879
7980
Internal routines
@@ -121,7 +122,7 @@
121122

122123
logger = logging.getLogger(__name__)
123124

124-
"""for convenience""" # TODO: contribute to ophyd?
125+
"""for convenience""" # TODO: contribute to ophyd?
125126
SCALER_AUTOCOUNT_MODE = 1
126127

127128

@@ -1322,60 +1323,81 @@ class KohzuSeqCtl_Monochromator(Device):
13221323
crystal_type = Component(EpicsSignal, "BraggTypeMO")
13231324

13241325

1325-
class TemperatureController_Base(Device):
1326+
class ProcessController(Device):
13261327
"""
1327-
common parts of temperature controller support
1328+
common parts of a process controller support
1329+
1330+
A process controller keeps a signal (a readback value such as
1331+
temperature, vacuum, himdity, etc.) as close as possible
1332+
to a target (set point) value. It has additional fields
1333+
that describe parameters specific to the controller such
1334+
as PID loop, on/off, applied controller power, and other
1335+
details.
1336+
1337+
This is a base class to standardize the few common terms
1338+
used to command and record the target and readback values
1339+
of a process controller.
1340+
1341+
Subclasses should redefine (override) `controller_name`,
1342+
``signal``, ``target``, and ``units`` such as the example below.
1343+
Also set values for ``tolerance``, ``report_interval_s``, and
1344+
``poll_s`` suitable for the specific controller used.
1345+
1346+
*Floats*: ``signal``, ``target`', and ``tolerance`` will be
1347+
considered as floating point numbers in the code.
1348+
1349+
It is assumed in "meth"`settled()` that: ``|signal - target| <= tolerance``.
1350+
Override this *property* method if a different decision is needed.
13281351
13291352
EXAMPLE::
13301353
1331-
class MyLinkam(TemperatureController_Base):
1332-
controller_name = "MyLinkam"
1333-
temperature = Component(EpicsSignalRO, "temp")
1334-
set_point = Component(EpicsSignal, "setLimit", kind="omitted")
1354+
class MyLinkam(ProcessController):
1355+
controller_name = "MyLinkam Controller"
1356+
signal = Component(EpicsSignalRO, "temp")
1357+
target = Component(EpicsSignal, "setLimit", kind="omitted")
1358+
units = Component(Signal, kind="omitted", value="C")
13351359
13361360
controller = MyLinkam("my:linkam:", name="controller")
13371361
RE(controller.wait_until_settled(timeout=10))
13381362
1339-
controller.record_temperature()
1340-
print(f"{controller.controller_name} controller settled? {controller.settled}")
1363+
controller.record_signal()
1364+
print(f"{controller.controller_name} settled? {controller.settled}")
13411365
13421366
def rampUp_rampDown():
13431367
'''ramp temperature up, then back down'''
1344-
yield from controller.set_temperature(25, timeout=180)
1345-
controller.report_interval = 10 # change report interval to 10s
1368+
yield from controller.set_target(25, timeout=180)
1369+
controller.report_interval_s = 10 # change report interval to 10s
13461370
for i in range(10, 0, -1):
1347-
print(f"hold at (self.value:.2f)C, time remaining: {i}s")
1371+
print(f"hold at {self.value:.2f}{self.units.value}, time remaining: {i}s")
13481372
yield from bps.sleep(1)
1349-
yield from controller.set_temperature(0, timeout=180)
1373+
yield from controller.set_target(0, timeout=180)
13501374
13511375
RE(test_plan())
13521376
13531377
"""
13541378

1355-
controller_name = "TemperatureController_Base"
1356-
temperature = Component(Signal) # override in subclass
1357-
set_point = Component(Signal, kind="omitted") # override in subclass
1379+
controller_name = "ProcessController"
1380+
signal = Component(Signal) # override in subclass
1381+
target = Component(Signal, kind="omitted") # override in subclass
1382+
tolerance = Component(Signal, kind="omitted", value=1) # override in subclass
1383+
units = Component(Signal, kind="omitted", value="") # override in subclass
13581384

1359-
tolerance = 1 # requirement: |T - target| must be <= this, degree C
1360-
report_interval = 5 # time between reports during loop, s
1385+
tolerance = 1 # requirement: |signal - target| <= tolerance (see `settled()`)
1386+
report_interval_s = 5 # time between reports during loop, s
13611387
poll_s = 0.02 # time to wait during polling loop, s
13621388

1363-
def record_temperature(self):
1364-
"""write temperatures as comment"""
1365-
global specwriter
1366-
msg = f"{self.controller_name} Temperature: {self.value:.2f} C"
1367-
specwriter._cmt("event", msg)
1389+
def record_signal(self):
1390+
"""write signal to the console"""
1391+
msg = f"{self.controller_name} signal: {self.value:.2f}{self.units.value}"
13681392
print(msg)
1393+
return msg
13691394

1370-
def set_temperature(self, set_point, wait=True, timeout=None, timeout_fail=False):
1371-
"""change controller to new temperature set point"""
1372-
global specwriter
1395+
def set_target(self, target, wait=True, timeout=None, timeout_fail=False):
1396+
"""change controller to new signal set point"""
1397+
yield from bps.mv(self.target, target)
13731398

1374-
yield from bps.mv(self.set_point, set_point)
1375-
1376-
msg = f"Set {self.controller_name} Temperature to {set_point:.2f} C"
1399+
msg = f"Set {self.controller_name} target to {target:.2f}{self.units.value}"
13771400
print(msg)
1378-
specwriter._cmt("event", msg)
13791401

13801402
if wait:
13811403
yield from self.wait_until_settled(
@@ -1384,58 +1406,58 @@ def set_temperature(self, set_point, wait=True, timeout=None, timeout_fail=False
13841406

13851407
@property
13861408
def value(self):
1387-
"""shortcut to self.temperature.value"""
1388-
return self.temperature.value
1409+
"""shortcut to self.signal.value"""
1410+
return self.signal.value
13891411

13901412
@property
13911413
def settled(self):
1392-
"""Is temperature close enough to target?"""
1393-
diff = abs(self.temperature.get() - self.set_point.value)
1414+
"""Is signal close enough to target?"""
1415+
diff = abs(self.signal.get() - self.target.value)
13941416
return diff <= self.tolerance
13951417

13961418
def wait_until_settled(self, timeout=None, timeout_fail=False):
13971419
"""
1398-
wait for controller to reach target temperature
1420+
plan: wait for controller signal to reach target within tolerance
13991421
"""
14001422
# see: https://stackoverflow.com/questions/2829329/catch-a-threads-exception-in-the-caller-thread-in-python
14011423
t0 = time.time()
1402-
_st = DeviceStatus(self.temperature)
1403-
started = False
1404-
1405-
def changing_cb(value, timestamp, **kwargs):
1406-
if started and self.settled:
1407-
_st._finished(success=True)
1424+
_st = DeviceStatus(self.signal)
14081425

1409-
token = self.temperature.subscribe(changing_cb)
1410-
started = True
1411-
1412-
report = 0
1413-
while not _st.done and not self.settled:
1414-
elapsed = time.time() - t0
1415-
if timeout is not None and elapsed > timeout:
1416-
_st._finished(success=self.settled)
1417-
msg = f"Temperature Controller Timeout after {elapsed:.2f}s"
1418-
msg += f", target {self.set_point.value:.2f}C"
1419-
msg += f", now {self.temperature.get():.2f}C"
1420-
# msg += f", status={_st}"
1421-
print(msg)
1422-
if timeout_fail:
1423-
raise TimeoutError(msg)
1424-
continue
1425-
if elapsed >= report:
1426-
report += self.report_interval
1427-
msg = f"Waiting {elapsed:.1f}s"
1428-
msg += f" to reach {self.set_point.value:.2f}C"
1429-
msg += f", now {self.temperature.get():.2f}C"
1430-
print(msg)
1431-
yield from bps.sleep(self.poll_s)
1432-
1433-
if not _st.done and self.settled:
1434-
# just in case self.temperature already at temperature
1426+
if self.settled:
1427+
# just in case signal already at target
14351428
_st._finished(success=True)
1436-
1437-
self.temperature.unsubscribe(token)
1438-
self.record_temperature()
1429+
else:
1430+
started = False
1431+
1432+
def changing_cb(*args, **kwargs):
1433+
if started and self.settled:
1434+
_st._finished(success=True)
1435+
1436+
token = self.signal.subscribe(changing_cb)
1437+
started = True
1438+
report = 0
1439+
while not _st.done and not self.settled:
1440+
elapsed = time.time() - t0
1441+
if timeout is not None and elapsed > timeout:
1442+
_st._finished(success=self.settled)
1443+
msg = f"{self.controller_name} Timeout after {elapsed:.2f}s"
1444+
msg += f", target {self.target.value:.2f}{self.units.value}"
1445+
msg += f", now {self.signal.get():.2f}{self.units.value}"
1446+
print(msg)
1447+
if timeout_fail:
1448+
raise TimeoutError(msg)
1449+
continue
1450+
if elapsed >= report:
1451+
report += self.report_interval_s.value
1452+
msg = f"Waiting {elapsed:.1f}s"
1453+
msg += f" to reach {self.target.value:.2f}{self.units.value}"
1454+
msg += f", now {self.signal.get():.2f}{self.units.value}"
1455+
print(msg)
1456+
yield from bps.sleep(self.poll_s)
1457+
1458+
self.signal.unsubscribe(token)
1459+
1460+
self.record_signal()
14391461
elapsed = time.time() - t0
14401462
print(f"Total time: {elapsed:.3f}s, settled:{_st.success}")
14411463

0 commit comments

Comments
 (0)