Script execution
This commit is contained in:
949
script/scan_guard.py
Normal file
949
script/scan_guard.py
Normal file
@@ -0,0 +1,949 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Scan guard utility for PEARL scans
|
||||
|
||||
This utility watches the scan facilities at X03DA
|
||||
and responds to beam loss and other incidents:
|
||||
|
||||
IMPLEMENTED:
|
||||
|
||||
* Beam loss: Pause the sscan records.
|
||||
* Beam loss: Pause the Scienta analyser.
|
||||
* Beam loss: Repeat the last Scienta measurement when the beam is restored.
|
||||
* Beam loss: Close/open the station shutter.
|
||||
* Beam loss: Independent low and high trip levels.
|
||||
* Beam loss: Beam loss actions are done only if beamline operation is set to "unattended"
|
||||
* Beam loss: Closed absorber or front end shutter will also trigger a beam loss
|
||||
|
||||
PLANNED:
|
||||
|
||||
* Update the data folder and file names according to the current date.
|
||||
* Serve operation parameters as EPICS channels
|
||||
|
||||
TODO:
|
||||
|
||||
Author:
|
||||
Matthias Muntwiler
|
||||
|
||||
Created:
|
||||
Oct 03, 2013
|
||||
|
||||
Copyright:
|
||||
(c) 2013 Paul Scherrer Institut
|
||||
|
||||
$Id: scan_guard.py 43 2015-10-07 16:38:05Z muntwiler_m $
|
||||
"""
|
||||
import time
|
||||
import logging
|
||||
import epicsPV
|
||||
|
||||
##### MonitoredDevice class #####
|
||||
|
||||
class MonitoredDevice(object):
|
||||
"""
|
||||
Base class for devices which detect beam loss/return.
|
||||
"""
|
||||
|
||||
def __init__(self, name):
|
||||
"""
|
||||
self.name = device name from which the PV names can be derived.
|
||||
self.pv = an epicsPV.epicsPV object connecting to an EPICS PV
|
||||
self.device_active = the device is consulted to decide whether the beam is on or off.
|
||||
self.severity = log level to report on beam loss (logging.WARNING, logging.ERROR, or logging.CRITICAL)
|
||||
"""
|
||||
self.name = name
|
||||
self.pv = None
|
||||
self.device_active = True
|
||||
self.severity = logging.WARNING
|
||||
self._loss_log_func = logging.warning
|
||||
|
||||
def connect(self):
|
||||
"""
|
||||
Connects to the EPICS PVs.
|
||||
Name of PVs can be derived from self.name.
|
||||
pend_io() should be done by caller.
|
||||
"""
|
||||
logging.info("setting up monitored device: %s", self.name)
|
||||
if self.severity == logging.ERROR:
|
||||
self._loss_log_func = logging.error
|
||||
elif self.severity == logging.CRITICAL:
|
||||
self._loss_log_func = logging.critical
|
||||
else:
|
||||
self._loss_log_func = logging.warning
|
||||
|
||||
def poll(self):
|
||||
"""
|
||||
Polls the PVs used by the class if necessary.
|
||||
"""
|
||||
pass
|
||||
|
||||
def set_monitors(self):
|
||||
"""
|
||||
Sets monitors on the PVs used by the class if necessary.
|
||||
"""
|
||||
pass
|
||||
|
||||
def beam_okay(self, last_beam_status):
|
||||
"""
|
||||
Returns True if the beam is okay from the perspective of this device, False otherwise.
|
||||
"""
|
||||
return False
|
||||
|
||||
|
||||
##### TripDevice class #####
|
||||
|
||||
class TripDevice(MonitoredDevice):
|
||||
"""
|
||||
Base class for devices which detect beam loss/return by checking that a float value is within the allowed range.
|
||||
"""
|
||||
|
||||
def __init__(self, name, trip_lo, trip_hi, dead_band):
|
||||
"""
|
||||
name = complete name of the EPICS PV to be monitored
|
||||
trip_lo = beam loss is triggered if value falls below this level
|
||||
trip_hi = beam loss is triggered if value rises above this level
|
||||
dead_band = beam is restored if value returns to within trip_lo + dead_band ... trip_hi - dead_band
|
||||
"""
|
||||
super(TripDevice, self).__init__(name)
|
||||
self.trip_lo = trip_lo
|
||||
self.trip_hi = trip_hi
|
||||
self.dead_band = dead_band
|
||||
|
||||
def connect(self):
|
||||
self.pv = epicsPV.epicsPV(self.name, wait=0)
|
||||
logging.info("%s low trip %g", self.name, self.trip_lo)
|
||||
logging.info("%s high trip %g", self.name, self.trip_hi)
|
||||
|
||||
def set_monitors(self):
|
||||
self.pv.array_get()
|
||||
self.pv.setMonitor()
|
||||
|
||||
def poll(self):
|
||||
logging.debug("caget %s: %g", self.pv.pvname, self.pv.getValue())
|
||||
|
||||
def beam_okay(self, last_beam_status):
|
||||
if not self.device_active:
|
||||
return True
|
||||
|
||||
val = self.pv.getValue()
|
||||
if last_beam_status:
|
||||
result = self.trip_lo <= val <= self.trip_hi
|
||||
else:
|
||||
result = self.trip_lo + self.dead_band <= val <= self.trip_hi - self.dead_band
|
||||
|
||||
logging.debug("beam_okay = %s (%g mA)", str(result), val)
|
||||
if last_beam_status and not result:
|
||||
self._loss_log_func("%s trip (%g mA)", self.name, val)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
##### BeamDrop class #####
|
||||
|
||||
class BeamDrop(MonitoredDevice):
|
||||
def __init__(self, name):
|
||||
super(BeamDrop, self).__init__(name)
|
||||
|
||||
def connect(self):
|
||||
self.pv = epicsPV.epicsPV(self.name, wait=0)
|
||||
|
||||
def set_monitors(self):
|
||||
self.pv.array_get()
|
||||
self.pv.setMonitor()
|
||||
|
||||
def poll(self):
|
||||
logging.debug("caget %s: %g", self.pv.pvname, self.pv.getValue())
|
||||
|
||||
def beam_okay(self, last_beam_status):
|
||||
if not self.device_active:
|
||||
return True
|
||||
|
||||
val = self.pv.getValue()
|
||||
result = val == 0
|
||||
|
||||
logging.debug("operation_mode_okay() = %s (%g)", str(result), val)
|
||||
if last_beam_status and not result:
|
||||
self._loss_log_func("operation mode trip (%g)", val)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
##### BeamlineMode class #####
|
||||
|
||||
class BeamlineMode(MonitoredDevice):
|
||||
OFFLINE = 0
|
||||
ATTENDED = 1
|
||||
UNATTENDED = 2
|
||||
|
||||
def __init__(self, name):
|
||||
super(BeamlineMode, self).__init__(name)
|
||||
|
||||
def connect(self):
|
||||
self.pv = epicsPV.epicsPV(self.name, wait=0)
|
||||
|
||||
def set_monitors(self):
|
||||
self.pv.array_get()
|
||||
self.pv.setMonitor()
|
||||
|
||||
def poll(self):
|
||||
logging.debug("caget %s: %g", self.pv.pvname, self.pv.getValue())
|
||||
|
||||
def beam_okay(self, last_beam_status):
|
||||
if not self.device_active:
|
||||
return True
|
||||
|
||||
val = self.pv.getValue()
|
||||
result = val == self.ATTENDED or val == self.UNATTENDED
|
||||
|
||||
logging.debug("beamline_mode_okay = %s (%g)", str(result), val)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
##### FrontendShutter class #####
|
||||
|
||||
class FrontendShutter(MonitoredDevice):
|
||||
"""
|
||||
Monitors a front end shutter.
|
||||
This can be any on/off device with a {name}:OPEN_EPS readback
|
||||
which reports 1 in the open state, and 0 in the closed state.
|
||||
"""
|
||||
CLOSED = 0
|
||||
OPEN = 1
|
||||
|
||||
def __init__(self, name):
|
||||
super(FrontendShutter, self).__init__(name)
|
||||
|
||||
def connect(self):
|
||||
"""
|
||||
Connects to the process variables.
|
||||
name = name of the shutter, e.g. "X03DA-FE-PH1"
|
||||
"""
|
||||
MonitoredDevice.connect(self)
|
||||
self.pv = epicsPV.epicsPV(self.name + ":OPEN_EPS", wait=0)
|
||||
|
||||
def set_monitors(self):
|
||||
self.pv.array_get()
|
||||
self.pv.setMonitor()
|
||||
|
||||
def poll(self):
|
||||
"""
|
||||
Polls the shutter position
|
||||
"""
|
||||
logging.debug("caget %s: %g", self.pv.pvname, self.pv.getValue())
|
||||
|
||||
def beam_okay(self, last_beam_status):
|
||||
"""
|
||||
Returns True if the shutter is open, False otherwise.
|
||||
"""
|
||||
if not self.device_active:
|
||||
return True
|
||||
|
||||
val = self.pv.getValue()
|
||||
result = val == self.OPEN
|
||||
|
||||
logging.debug("shutter %s = %s (%g)", self.name, str(result), val)
|
||||
if last_beam_status and not result:
|
||||
self._loss_log_func("shutter trip %s", self.name)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
##### ControlledDevice class #####
|
||||
|
||||
class ControlledDevice(object):
|
||||
"""
|
||||
Base class for devices which are controlled in response to beam loss/return.
|
||||
"""
|
||||
|
||||
def __init__(self, name):
|
||||
"""
|
||||
self.name = device name from which the PV names can be derived.
|
||||
self.device_active = the device participated in the measurement before it was paused.
|
||||
self.paused = the device is paused due to a call to the self.pause() method.
|
||||
"""
|
||||
self.name = name
|
||||
self.device_active = False
|
||||
self.paused = False
|
||||
|
||||
def connect(self):
|
||||
"""
|
||||
Connects to the EPICS PVs.
|
||||
Name of PVs can be derived from self.name.
|
||||
pend_io() should be done by caller.
|
||||
"""
|
||||
logging.info("setting up controlled device: %s", self.name)
|
||||
pass
|
||||
|
||||
def poll(self):
|
||||
"""
|
||||
Polls the PVs used by the class if necessary.
|
||||
"""
|
||||
pass
|
||||
|
||||
def set_monitors(self):
|
||||
"""
|
||||
Sets monitors on the PVs used by the class if necessary.
|
||||
"""
|
||||
pass
|
||||
|
||||
def pause(self):
|
||||
"""
|
||||
Pauses the device, e.g. in response to beam loss.
|
||||
The method should update self.device_active and self.paused.
|
||||
"""
|
||||
pass
|
||||
|
||||
def resume(self):
|
||||
"""
|
||||
Resumes the device.
|
||||
This method should act only if the previous pause() actually changed the device,
|
||||
i.e. self.pause == True.
|
||||
The method must reset self.paused.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
##### ScanRecord class #####
|
||||
|
||||
class ScanRecord(ControlledDevice):
|
||||
def __init__(self, name):
|
||||
"""
|
||||
self.faze: epicsPV.epicsPV connecting to the FAZE field of the scan record.
|
||||
self.wait: epicsPV.epicsPV connecting to the WAIT field of the scan record.
|
||||
"""
|
||||
super(ScanRecord, self).__init__(name)
|
||||
self.faze = None
|
||||
self.wait = None
|
||||
|
||||
def connect(self):
|
||||
"""
|
||||
Connects to the process variables of the scan record
|
||||
name = complete name of the scan record
|
||||
"""
|
||||
ControlledDevice.connect(self)
|
||||
self.faze = epicsPV.epicsPV(self.name + '.FAZE', wait=0)
|
||||
self.wait = epicsPV.epicsPV(self.name + '.WAIT', wait=0)
|
||||
|
||||
def set_monitors(self):
|
||||
# before we can set a monitor, we have to get the value once.
|
||||
self.faze.array_get()
|
||||
self.faze.setMonitor()
|
||||
self.wait.array_get()
|
||||
|
||||
def poll(self):
|
||||
logging.debug("caget %s: %g", self.faze.pvname, self.faze.getValue())
|
||||
logging.debug("caget %s: %g", self.wait.pvname, self.wait.getValue())
|
||||
|
||||
def pause(self):
|
||||
self.device_active = self.faze.getValue() >= 1
|
||||
if self.device_active:
|
||||
logging.info("pausing scan on %s", self.name)
|
||||
self.wait.array_put(1)
|
||||
self.wait.pend_io()
|
||||
self.paused = True
|
||||
else:
|
||||
self.paused = False
|
||||
|
||||
def resume(self):
|
||||
if self.paused:
|
||||
# do not resume if the user has aborted the scan
|
||||
if self.faze.getValue() >= 1:
|
||||
logging.info("resuming scan on %s", self.name)
|
||||
self.wait.array_put(0)
|
||||
self.wait.pend_io()
|
||||
else:
|
||||
logging.warning("scan on %s aborted by user", self.name)
|
||||
self.paused = False
|
||||
|
||||
|
||||
##### AreaDetector class #####
|
||||
|
||||
class AreaDetector(ControlledDevice):
|
||||
"""
|
||||
This class pauses and resumes an Area Detector device.
|
||||
While pausing, array callbacks are disabled
|
||||
so that the incomplete measurement is not passed to the plugins.
|
||||
If the detector resumes before the scan advances,
|
||||
the incomplete measurement is effectively repeated.
|
||||
|
||||
Pause: Callbacks are disabled. Acquisition is aborted.
|
||||
Resume: Callbacks are enabled. Acquisition is restarted if it was interrupted.
|
||||
|
||||
The detector is paused only if all of the following conditions are true,
|
||||
otherwise pause() will have no effect:
|
||||
1) Array callbacks are enabled on cam1
|
||||
2) HDF1 plugin is capturing
|
||||
|
||||
resume() has no effect unless the previous pause() stopped the detector.
|
||||
"""
|
||||
|
||||
def __init__(self, name):
|
||||
super(AreaDetector, self).__init__(name)
|
||||
# indicates whether we stopped a running acquisition when the pause started
|
||||
self.acq_active = False
|
||||
self.ArrayCallbacks = None
|
||||
self.DetectorState = None
|
||||
self.Acquire = None
|
||||
self.AcquireTime = None
|
||||
self.Capturing = None
|
||||
|
||||
def connect(self):
|
||||
"""
|
||||
Connects to the process variables.
|
||||
name = prefix of the area detector (everything before "cam1", e.g. "X03DA-SCIENTA")
|
||||
"""
|
||||
ControlledDevice.connect(self)
|
||||
self.ArrayCallbacks = epicsPV.epicsPV(self.name + ':cam1:ArrayCallbacks', wait=0)
|
||||
self.DetectorState = epicsPV.epicsPV(self.name + ':cam1:DetectorState_RBV', wait=0)
|
||||
self.Acquire = epicsPV.epicsPV(self.name + ':cam1:Acquire', wait=0)
|
||||
self.AcquireTime = epicsPV.epicsPV(self.name + ':cam1:AcquireTime_RBV', wait=0)
|
||||
self.Capturing = epicsPV.epicsPV(self.name + ':HDF1:Capture_RBV', wait=0)
|
||||
|
||||
def poll(self):
|
||||
"""
|
||||
Polls the detector state.
|
||||
This class doesn't set monitors on the PVs.
|
||||
Thus, we have to explicitly poll them before reading their values.
|
||||
"""
|
||||
self.DetectorState.array_get()
|
||||
logging.debug("caget %s: %g", self.DetectorState.pvname, self.DetectorState.getValue())
|
||||
self.ArrayCallbacks.array_get()
|
||||
logging.debug("caget %s: %g", self.ArrayCallbacks.pvname, self.ArrayCallbacks.getValue())
|
||||
self.Capturing.array_get()
|
||||
logging.debug("caget %s: %g", self.Capturing.pvname, self.Capturing.getValue())
|
||||
|
||||
def pause(self):
|
||||
"""
|
||||
Pauses the area detector.
|
||||
The goal is that no corrupted data gets into a data file in stream mode.
|
||||
|
||||
The detector is considered active if the HDF5 plugin is in capture mode,
|
||||
and if the array callbacks are enabled. In this case,
|
||||
1) the callbacks are disabled so that the corrupted data is not passed on to the plugins,
|
||||
2) the acquisition is stopped if running.
|
||||
"""
|
||||
self.device_active = ((self.ArrayCallbacks.getValue() == 1) and
|
||||
(self.Capturing.getValue() == 1))
|
||||
self.acq_active = self.DetectorState.getValue() == 1
|
||||
logging.debug("caget %s: %g", self.DetectorState.pvname, self.DetectorState.getValue())
|
||||
logging.debug("acq_active = %s", str(self.acq_active))
|
||||
if self.device_active:
|
||||
logging.info("disabling callbacks on %s", self.name)
|
||||
self.ArrayCallbacks.array_put(0)
|
||||
self.Acquire.pend_io()
|
||||
logging.info("stopping acquisition on %s", self.name)
|
||||
self.Acquire.array_put(0)
|
||||
self.Acquire.pend_io()
|
||||
self.paused = True
|
||||
else:
|
||||
logging.debug("device %s not active", self.name)
|
||||
self.paused = False
|
||||
|
||||
def resume(self):
|
||||
"""
|
||||
Resumes the area detector.
|
||||
|
||||
1) The callbacks are re-enabled.
|
||||
2) If the acquisition was interruped by the pause, it is restarted
|
||||
unless the user has stopped the capture mode.
|
||||
"""
|
||||
if self.paused:
|
||||
self.AcquireTime.array_get()
|
||||
timeout = self.AcquireTime.getValue() * 2.0 + 10.0
|
||||
self.wait_idle(timeout=timeout)
|
||||
logging.info("re-enabling callbacks on %s", self.name)
|
||||
self.ArrayCallbacks.array_put(1)
|
||||
logging.debug("acq_active = %s", str(self.acq_active))
|
||||
|
||||
self.Capturing.array_get()
|
||||
if self.Capturing.getValue() >= 1:
|
||||
# note: we need to restart the acquisition regardless of previous state.
|
||||
# if self.acq_active:
|
||||
logging.info("repeating acquisition on %s", self.name)
|
||||
self.Acquire.array_put(1)
|
||||
self.Acquire.pend_io()
|
||||
if self.wait_acquire(timeout=10.0):
|
||||
self.wait_idle(timeout=timeout)
|
||||
else:
|
||||
# user stopped the capture
|
||||
logging.warning("file capture on %s aborted by user", self.name)
|
||||
self.paused = False
|
||||
|
||||
def wait_idle(self, timeout=60.0):
|
||||
"""
|
||||
waits for the detector to become idle.
|
||||
returns True if detector is idle at the end,
|
||||
False if the timeout exceeded while the detector is not idle.
|
||||
"""
|
||||
self.DetectorState.array_get()
|
||||
if self.DetectorState.getValue() == 1:
|
||||
logging.info("waiting until acquisition stops on %s", self.name)
|
||||
if self.DetectorState.getValue() > 1:
|
||||
logging.error("unexpected detector state on %s: %g", self.name, self.DetectorState.getValue())
|
||||
while (self.DetectorState.getValue() >= 1) and (timeout > 0.0):
|
||||
self.DetectorState.pend_event(timeout=1.0)
|
||||
self.DetectorState.array_get()
|
||||
timeout -= 1.0
|
||||
if timeout <= 0.0:
|
||||
logging.error("timeout while waiting for detector %s to become idle. current state: %g", self.name,
|
||||
self.DetectorState.getValue())
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def wait_acquire(self, timeout=60.0):
|
||||
"""
|
||||
waits for the detector to start acquisition.
|
||||
returns True if detector is acquiring at the end,
|
||||
False if the timeout exceeded while the detector is idle.
|
||||
"""
|
||||
self.DetectorState.array_get()
|
||||
if self.DetectorState.getValue() < 1:
|
||||
logging.info("waiting until acquisition starts on %s", self.name)
|
||||
if self.DetectorState.getValue() > 1:
|
||||
logging.error("unexpected detector state on %s: %g", self.name, self.DetectorState.getValue())
|
||||
while (self.DetectorState.getValue() < 1) and (timeout > 0.0):
|
||||
self.DetectorState.pend_event(timeout=1.0)
|
||||
self.DetectorState.array_get()
|
||||
timeout -= 1.0
|
||||
if timeout <= 0.0:
|
||||
logging.error("timeout while waiting for detector %s. current state: %g", self.name,
|
||||
self.DetectorState.getValue())
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
##### AnalogChannels class #####
|
||||
|
||||
class AnalogChannels(ControlledDevice):
|
||||
"""
|
||||
This class pauses and resumes the Analog Channels device.
|
||||
|
||||
We don't have much control over this devices. All we can do is trigger it.
|
||||
Triggering is allowed at any time even if the previous interval hasn't ended.
|
||||
|
||||
Pause: no action.
|
||||
Resume: Device is triggered.
|
||||
|
||||
resume() has no effect unless the previous pause() stopped the detector.
|
||||
"""
|
||||
|
||||
def __init__(self, name):
|
||||
super(AnalogChannels, self).__init__(name)
|
||||
self.Trigger = None
|
||||
|
||||
def connect(self):
|
||||
"""
|
||||
Connects to the process variables.
|
||||
name = prefix of the analog channels (everything before the colon ":", e.g. "X03DA-OP-10ADC")
|
||||
"""
|
||||
ControlledDevice.connect(self)
|
||||
self.Trigger = epicsPV.epicsPV(self.name + ':TRG', wait=0)
|
||||
|
||||
def poll(self):
|
||||
"""
|
||||
Polls the detector state.
|
||||
This class doesn't set monitors on the PVs.
|
||||
Thus, we have to explicitly poll them before reading their values.
|
||||
"""
|
||||
self.Trigger.array_get()
|
||||
logging.debug("caget %s: %g", self.Trigger.pvname, self.Trigger.getValue())
|
||||
|
||||
def pause(self):
|
||||
"""
|
||||
Pauses the detector.
|
||||
This method does not change the state of the device.
|
||||
|
||||
It is not possible (and not necessary) to stop the device.
|
||||
Writing any value to self.Trigger will trigger a new measurement at any time.
|
||||
"""
|
||||
self.device_active = True
|
||||
self.paused = True
|
||||
|
||||
def resume(self):
|
||||
"""
|
||||
Triggers the detector.
|
||||
"""
|
||||
if self.paused:
|
||||
logging.info("triggering analog channels %s", self.name)
|
||||
self.Trigger.array_put(1)
|
||||
self.paused = False
|
||||
|
||||
|
||||
##### StationShutter class #####
|
||||
|
||||
class StationShutter(ControlledDevice):
|
||||
CLOSED = 2
|
||||
MOVING = 4
|
||||
OPEN = 5
|
||||
|
||||
def __init__(self, name):
|
||||
super(StationShutter, self).__init__(name)
|
||||
self.ShutterOpen = None
|
||||
self.ShutterClose = None
|
||||
self.ShutterRbv = None
|
||||
|
||||
def connect(self):
|
||||
"""
|
||||
Connects to the process variables.
|
||||
name = name of the station shutter, e.g. "X03DA-OP-ST1"
|
||||
"""
|
||||
ControlledDevice.connect(self)
|
||||
self.ShutterOpen = epicsPV.epicsPV(self.name + ":WT_SET_OPEN", wait=0)
|
||||
self.ShutterClose = epicsPV.epicsPV(self.name + ":WT_SET_CLOSE", wait=0)
|
||||
self.ShutterRbv = epicsPV.epicsPV(self.name + ":POSITION", wait=0)
|
||||
|
||||
def set_monitors(self):
|
||||
self.ShutterRbv.array_get()
|
||||
self.ShutterRbv.setMonitor()
|
||||
|
||||
def poll(self):
|
||||
"""
|
||||
Polls the shutter position
|
||||
"""
|
||||
logging.debug("caget %s: %g", self.ShutterRbv.pvname, self.ShutterRbv.getValue())
|
||||
|
||||
def pause(self):
|
||||
"""
|
||||
Closes the shutter.
|
||||
"""
|
||||
self.device_active = self.ShutterRbv.getValue() == self.OPEN
|
||||
if self.device_active:
|
||||
logging.info("closing shutter %s", self.name)
|
||||
self.set_shutter(self.CLOSED)
|
||||
self.paused = True
|
||||
else:
|
||||
self.paused = False
|
||||
|
||||
def resume(self):
|
||||
"""
|
||||
Opens the shutter if it was open before the pause.
|
||||
"""
|
||||
if self.paused:
|
||||
logging.info("opening shutter %s", self.name)
|
||||
self.set_shutter(self.OPEN)
|
||||
self.paused = False
|
||||
|
||||
def set_shutter(self, newpos):
|
||||
"""
|
||||
Opens or closes the shutter.
|
||||
|
||||
newpos = StationShutter.OPEN or StationShutter.CLOSED
|
||||
"""
|
||||
# try 5 times before giving up
|
||||
tries = 5
|
||||
while (self.ShutterRbv.getValue() != newpos) and (tries > 0):
|
||||
oldpos = self.ShutterRbv.getValue()
|
||||
curpos = oldpos
|
||||
logging.debug("shutter position before trigger: %g", curpos)
|
||||
# send the command sequence
|
||||
self.ShutterOpen.array_put(0)
|
||||
self.ShutterClose.array_put(0)
|
||||
self.ShutterRbv.pend_event(timeout=0.01)
|
||||
if newpos == self.OPEN:
|
||||
self.ShutterOpen.array_put(1)
|
||||
else:
|
||||
self.ShutterClose.array_put(1)
|
||||
self.ShutterRbv.pend_event(timeout=0.5)
|
||||
self.ShutterOpen.array_put(0)
|
||||
self.ShutterClose.array_put(0)
|
||||
self.ShutterRbv.pend_event(timeout=0.01)
|
||||
curpos = self.ShutterRbv.getValue()
|
||||
logging.debug("shutter position after command: %g", curpos)
|
||||
# wait until it starts moving
|
||||
timeout = 2.0
|
||||
while (self.ShutterRbv.getValue() == oldpos) and (timeout > 0.0):
|
||||
self.ShutterRbv.pend_event(timeout=0.1)
|
||||
timeout -= 0.1
|
||||
# wait while it is moving
|
||||
timeout = 5.0
|
||||
while (self.ShutterRbv.getValue() == self.MOVING) and (timeout > 0.0):
|
||||
self.ShutterRbv.pend_event(timeout=0.1)
|
||||
timeout -= 0.1
|
||||
# wait longer if the readback is unexpected
|
||||
timeout = 10.0
|
||||
while (self.ShutterRbv.getValue() not in [self.OPEN, self.CLOSED]) and (timeout > 0.0):
|
||||
self.ShutterRbv.pend_event(timeout=0.5)
|
||||
timeout -= 0.5
|
||||
curpos = self.ShutterRbv.getValue()
|
||||
logging.debug("shutter position after wait: %g", curpos)
|
||||
tries -= 1
|
||||
|
||||
curpos = self.ShutterRbv.getValue()
|
||||
result = curpos == newpos
|
||||
if not result:
|
||||
logging.error("failed to set station shutter (requested %g, actual %g)", newpos, curpos)
|
||||
return result
|
||||
|
||||
|
||||
##### ScanGuard class #####
|
||||
|
||||
class ScanGuard(object):
|
||||
"""
|
||||
Info strings for operation mode.
|
||||
Similar to those used by ACOAU-ACCU:OP-MODE without spaces and punctuation.
|
||||
|
||||
Notes:
|
||||
OP-MODE = 5 is signalled for orbit-feedback problems.
|
||||
"""
|
||||
OPMODES = ["MachineDown", "InjStopped", "Accumulating_", "Accumulating", "TopUpReady", "LightAvailable_",
|
||||
"LightAvailable"]
|
||||
|
||||
def __init__(self):
|
||||
# list of epicsPV objects monitored by this class
|
||||
self.monitors = []
|
||||
# list of ControlledDevice objects to be controlled
|
||||
self.devices = []
|
||||
# False = test mode, do not actually send out commands
|
||||
self.controls_active = False
|
||||
# True = beam is okay
|
||||
self.beam_ok = False
|
||||
# True while executing the main loop
|
||||
self.running = False
|
||||
# For test and demo, simulate a beam loss after 15 seconds.
|
||||
self.test = False
|
||||
self.test_time_seconds = 15
|
||||
self.test_start_seconds = time.time()
|
||||
|
||||
# for testing, use a local PV
|
||||
# self._ringcurrent = TripDevice('X03DA-OP-EXS:AP', 10.0, 500.0, 10.0)
|
||||
self._ringcurrent = TripDevice('ARIDI-PCT:CURRENT', 390.0, 410.0, 10.0)
|
||||
self.monitors.append(self._ringcurrent)
|
||||
# guard against erratic motors
|
||||
self._fmu_rx = TripDevice('X03DA-OP-FMU:oRx', -1.0, 1.0, 0.1)
|
||||
self._fmu_rx.severity = logging.ERROR
|
||||
self.monitors.append(self._fmu_rx)
|
||||
self._fmu_ry = TripDevice('X03DA-OP-FMU:oRy', -1.1, 0.9, 0.1)
|
||||
self._fmu_ry.severity = logging.ERROR
|
||||
self.monitors.append(self._fmu_ry)
|
||||
self._fmu_rz = TripDevice('X03DA-OP-FMU:oRz', -0.5, 0.5, 0.1)
|
||||
self._fmu_rz.severity = logging.ERROR
|
||||
self.monitors.append(self._fmu_rz)
|
||||
# self.operation_mode = OperationMode('ACOAU-ACCU:OP-MODE')
|
||||
# self.monitors.append(self.operation_mode)
|
||||
self._beam_drop = BeamDrop('ACOAU-ACCU:OP-BEAMDROP')
|
||||
self.monitors.append(self._beam_drop)
|
||||
self._beamline_mode = BeamlineMode('ACOAU-ACCU:OP-X03DA')
|
||||
self.monitors.append(self._beamline_mode)
|
||||
self._frontend_shutter = FrontendShutter('X03DA-FE-PH1')
|
||||
self.monitors.append(self._frontend_shutter)
|
||||
self._frontend_absorber = FrontendShutter('X03DA-FE-AB1')
|
||||
self.monitors.append(self._frontend_absorber)
|
||||
|
||||
# devices are paused in the order set here
|
||||
# they are resumed in reversed order
|
||||
self.devices.append(ScanRecord('X03DA-PC:scan1'))
|
||||
self.devices.append(ScanRecord('X03DA-PC:scan2'))
|
||||
self.devices.append(ScanRecord('X03DA-PC:scan3'))
|
||||
self.devices.append(ScanRecord('X03DA-PC:scan4'))
|
||||
# how can we detect whether channels are available?
|
||||
self.devices.append(AreaDetector('X03DA-SCIENTA'))
|
||||
self.devices.append(AnalogChannels('X03DA-OP-10ADC'))
|
||||
self.devices.append(StationShutter('X03DA-OP-ST1'))
|
||||
|
||||
@property
|
||||
def ringcurrent_trip_lo(self):
|
||||
"""
|
||||
trip if ring current falls below this level.
|
||||
"""
|
||||
return self._ringcurrent.trip_lo
|
||||
|
||||
@ringcurrent_trip_lo.setter
|
||||
def ringcurrent_trip_lo(self, value):
|
||||
self._ringcurrent.trip_lo = value
|
||||
|
||||
@property
|
||||
def ringcurrent_trip_hi(self):
|
||||
"""
|
||||
trip if ring current rises above this level.
|
||||
"""
|
||||
return self._ringcurrent.trip_hi
|
||||
|
||||
@ringcurrent_trip_hi.setter
|
||||
def ringcurrent_trip_hi(self, value):
|
||||
self._ringcurrent.trip_hi = value
|
||||
|
||||
@property
|
||||
def ringcurrent_dead_band(self):
|
||||
"""
|
||||
resume if ring current returns above ringcurrent_trip_lo + ringcurrent_dead_band.
|
||||
"""
|
||||
return self._ringcurrent.dead_band
|
||||
|
||||
@ringcurrent_dead_band.setter
|
||||
def ringcurrent_dead_band(self, value):
|
||||
self._ringcurrent.dead_band = value
|
||||
|
||||
@property
|
||||
def unattended_only(self):
|
||||
"""
|
||||
Scan guard is active during unattended operation only
|
||||
"""
|
||||
return self._beamline_mode.device_active
|
||||
|
||||
@unattended_only.setter
|
||||
def unattended_only(self, value):
|
||||
self._beamline_mode.device_active = value
|
||||
|
||||
def connect(self):
|
||||
for monitor in self.monitors:
|
||||
monitor.connect()
|
||||
for device in self.devices:
|
||||
device.connect()
|
||||
|
||||
# Wait for all PVs to connect
|
||||
try:
|
||||
self._ringcurrent.pv.pend_io()
|
||||
except:
|
||||
logging.error('EPICS channels not available')
|
||||
return False
|
||||
|
||||
# get first value, initialize data structures, set monitors
|
||||
for monitor in self.monitors:
|
||||
monitor.set_monitors()
|
||||
monitor.poll()
|
||||
for device in self.devices:
|
||||
device.set_monitors()
|
||||
device.poll()
|
||||
|
||||
return True
|
||||
|
||||
def mainloop(self):
|
||||
self._ringcurrent.pv.pend_io(timeout=0.01)
|
||||
self.beam_ok = self.test or self.check_beam()
|
||||
self.test_start_seconds = time.time()
|
||||
self.running = True
|
||||
|
||||
if self.unattended_only:
|
||||
logging.info("guard active in unattended operation mode only")
|
||||
else:
|
||||
logging.info("guard active regardless of beamline operation mode")
|
||||
if self.test:
|
||||
logging.info("test mode. simulated beam loss in %g seconds.", self.test_time_seconds)
|
||||
|
||||
while self.running:
|
||||
self._ringcurrent.pv.pend_event(timeout=1)
|
||||
new_beam_ok = self.check_beam()
|
||||
if new_beam_ok != self.beam_ok:
|
||||
self.beam_ok = new_beam_ok
|
||||
if new_beam_ok:
|
||||
logging.warning("beam restored")
|
||||
self.resume()
|
||||
else:
|
||||
logging.warning("beam down")
|
||||
self.pause()
|
||||
|
||||
def check_beam(self):
|
||||
beam_ok = self.beam_ok
|
||||
if self.test:
|
||||
rem_time = self.test_time_seconds - (time.time() - self.test_start_seconds)
|
||||
if rem_time < 0:
|
||||
beam_ok = not beam_ok
|
||||
self.test_start_seconds = time.time()
|
||||
# end test after one instance
|
||||
self.test = not beam_ok
|
||||
if self.test and not beam_ok:
|
||||
logging.info("simulated beam restore in %g seconds.", self.test_time_seconds)
|
||||
if not self.test:
|
||||
logging.info("test done - resuming normal operation")
|
||||
else:
|
||||
beam_ok = True
|
||||
for monitor in self.monitors:
|
||||
beam_ok = beam_ok and monitor.beam_okay(self.beam_ok)
|
||||
|
||||
logging.debug("check_beam() = %s", str(beam_ok))
|
||||
return beam_ok
|
||||
|
||||
def pause(self):
|
||||
for device in self.devices:
|
||||
device.poll()
|
||||
if self.controls_active:
|
||||
for device in self.devices:
|
||||
device.pause()
|
||||
|
||||
def resume(self):
|
||||
for device in reversed(self.devices):
|
||||
device.poll()
|
||||
if self.controls_active:
|
||||
for device in reversed(self.devices):
|
||||
device.resume()
|
||||
|
||||
|
||||
##### main frame #####
|
||||
|
||||
def main(args):
|
||||
nll = getattr(logging, args.loglevel.upper(), None)
|
||||
logging.basicConfig(format="%(asctime)s %(levelname)s: %(message)s", level=nll)
|
||||
logging.info("PEARL Scan Guard")
|
||||
logging.info("version 1.1.1")
|
||||
|
||||
guard = ScanGuard()
|
||||
guard.ringcurrent_trip_lo = args.riculo
|
||||
logging.debug(guard.ringcurrent_trip_lo)
|
||||
if args.ricuhi >= args.riculo:
|
||||
guard.ringcurrent_dead_band = args.ricuhi - guard.ringcurrent_trip_lo
|
||||
else:
|
||||
raise ValueError("invalid ring current trip arguments")
|
||||
logging.debug(guard.ringcurrent_trip_hi)
|
||||
guard.controls_active = args.controls_active
|
||||
logging.debug(guard.controls_active)
|
||||
guard.unattended_only = not args.attended
|
||||
logging.debug(guard.unattended_only)
|
||||
guard.test = args.test
|
||||
logging.debug(guard.test)
|
||||
guard.connect()
|
||||
try:
|
||||
pass
|
||||
guard.mainloop()
|
||||
except KeyboardInterrupt:
|
||||
logging.info("scan guard aborted by user")
|
||||
|
||||
|
||||
def test():
|
||||
"""
|
||||
ad-hoc test function
|
||||
"""
|
||||
logging.basicConfig(format="%(asctime)s %(levelname)s: %(message)s", level=logging.DEBUG)
|
||||
sh = StationShutter("X03DA-OP-ST1:")
|
||||
sh.connect()
|
||||
sh.ShutterRbv.pend_io()
|
||||
sh.set_monitors()
|
||||
return sh
|
||||
|
||||
|
||||
def operate():
|
||||
"""
|
||||
execute the scan guard in normal operation
|
||||
|
||||
this is the normal entry point of a stable version in normal operation.
|
||||
the function parameters should be stable.
|
||||
do not modify the parameters for tests, use the test() function instead.
|
||||
"""
|
||||
args.controls_active = True
|
||||
args.loglevel = logging.INFO
|
||||
main(args)
|
||||
|
||||
##### command line #####
|
||||
|
||||
import argparse
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Pause the measurements (scan records, Scienta detector) for beam loss")
|
||||
parser.add_argument("-o", "--ring-current-lo", type=float, default=390, dest="riculo",
|
||||
help="beam loss is triggered if ring current (mA) falls below this level")
|
||||
parser.add_argument("-i", "--ring-current-hi", type=float, default=400, dest="ricuhi",
|
||||
help="beam is restored if ring current (mA) rises above this level")
|
||||
parser.add_argument("-a", "--attended", action="store_true", dest="attended",
|
||||
help="guard beam during attended operation (default: unattended only)")
|
||||
parser.add_argument("-p", "--passive", action="store_false", dest="controls_active",
|
||||
help="check beam conditions but do not send out commands")
|
||||
parser.add_argument("-t", "--test", action="store_true", dest="test",
|
||||
help="test controls (simulate beam loss after 15 seconds)")
|
||||
parser.add_argument("-l", "--loglevel", dest="loglevel", default="INFO",
|
||||
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"])
|
||||
args = parser.parse_args()
|
||||
main(args)
|
||||
Reference in New Issue
Block a user