Moved aerotech to its own directory

This commit is contained in:
mohacsi_i 2024-03-08 14:20:11 +01:00
parent 3fd19f85ba
commit 413de89a83
6 changed files with 198 additions and 20 deletions

View File

@ -29,6 +29,6 @@ from .eiger9m_csaxs import Eiger9McSAXS
from .pilatus_csaxs import PilatuscSAXS from .pilatus_csaxs import PilatuscSAXS
from .falcon_csaxs import FalconcSAXS from .falcon_csaxs import FalconcSAXS
from .delay_generator_csaxs import DelayGeneratorcSAXS from .delay_generator_csaxs import DelayGeneratorcSAXS
from .AerotechAutomation1 import aa1Controller, aa1Tasks, aa1GlobalVariables, aa1GlobalVariableBindings, aa1AxisPsoDistance, aa1AxisDriveDataCollection, EpicsMotorX from .aerotech.AerotechAutomation1 import aa1Controller, aa1Tasks, aa1GlobalVariables, aa1GlobalVariableBindings, aa1AxisPsoDistance, aa1AxisDriveDataCollection, EpicsMotorX
# from .psi_detector_base import PSIDetectorBase, CustomDetectorMixin # from .psi_detector_base import PSIDetectorBase, CustomDetectorMixin

View File

@ -21,11 +21,10 @@ from typing import Union
from collections import OrderedDict from collections import OrderedDict
#class EpicsMotorX(EpicsMotor):
# pass
class EpicsMotorX(EpicsMotor): class EpicsMotorX(EpicsMotor):
""" Special motor class that provides flyer interface and progress bar.
"""
SUB_PROGRESS = "progress" SUB_PROGRESS = "progress"
def __init__(self, prefix="", *, name, kind=None, read_attrs=None, configuration_attrs=None, parent=None, **kwargs): def __init__(self, prefix="", *, name, kind=None, read_attrs=None, configuration_attrs=None, parent=None, **kwargs):
@ -64,19 +63,9 @@ class EpicsMotorX(EpicsMotor):
value=int(100*progress), max_value=max_value, done=int(np.isclose(max_value, progress, 1e-3)), ) value=int(100*progress), max_value=max_value, done=int(np.isclose(max_value, progress, 1e-3)), )
class EpicsSignalPassive(Device):
value = Component(EpicsSignalRO, "", kind=Kind.omitted)
proc = Component(EpicsSignal, ".PROC", kind=Kind.omitted, put_complete=True)
def get(self):
self.proc.set(1).wait()
return self.value.get()
class EpicsPassiveRO(EpicsSignalRO): class EpicsPassiveRO(EpicsSignalRO):
"""Special helper class to work around a bug in ophyd (caproto backend) """ Small helper class to read PVs that need to be processed first.
that reads CHAR array strigs as uint16 arrays.
""" """
def __init__(self, read_pv, *, string=False, name=None, **kwargs): def __init__(self, read_pv, *, string=False, name=None, **kwargs):
super().__init__(read_pv, string=string, name=name, **kwargs) super().__init__(read_pv, string=string, name=name, **kwargs)
self._proc = EpicsSignal(read_pv+".PROC", kind=Kind.omitted, put_complete=True) self._proc = EpicsSignal(read_pv+".PROC", kind=Kind.omitted, put_complete=True)
@ -113,14 +102,33 @@ class aa1Tasks(Device):
The place to manage tasks and AeroScript user files on the controller. The place to manage tasks and AeroScript user files on the controller.
You can read/write/compile/execute AeroScript files and also retrieve You can read/write/compile/execute AeroScript files and also retrieve
saved data files from the controller. saved data files from the controller. It will also work around an ophyd
bug that swallows failures.
Execute does not require to store the script in a file, it will compile Execution does not require to store the script in a file, it will compile
it and run it directly on a certain thread. But there's no way to it and run it directly on a certain thread. But there's no way to
retrieve the source code. retrieve the source code.
Write a text into a file on the aerotech controller and execute it with kickoff.
'''
script="var $axis as axis = ROTY\\nMoveAbsolute($axis, 42, 90)"
tsk = aa1Tasks(AA1_IOC_NAME+":TASK:", name="tsk")
tsk.wait_for_connection()
tsk.configure({'text': script, 'filename': "foobar.ascript", 'taskIndex': 4})
tsk.kickoff().wait()
'''
Just execute an ascript file already on the aerotech controller.
'''
tsk = aa1Tasks(AA1_IOC_NAME+":TASK:", name="tsk")
tsk.wait_for_connection()
tsk.configure({'filename': "foobar.ascript", 'taskIndex': 4})
tsk.kickoff().wait()
'''
""" """
SUB_PROGRESS = "progress" SUB_PROGRESS = "progress"
_failure = Component(EpicsSignalRO, "FAILURE", kind=Kind.hinted) _failure = Component(EpicsSignalRO, "FAILURE", auto_monitor=True, kind=Kind.hinted)
errStatus = Component(EpicsSignalRO, "ERRW", auto_monitor=True, kind=Kind.hinted) errStatus = Component(EpicsSignalRO, "ERRW", auto_monitor=True, kind=Kind.hinted)
warnStatus = Component(EpicsSignalRO, "WARNW", auto_monitor=True, kind=Kind.hinted) warnStatus = Component(EpicsSignalRO, "WARNW", auto_monitor=True, kind=Kind.hinted)
taskStates = Component(EpicsSignalRO, "STATES-RBV", auto_monitor=True, kind=Kind.hinted) taskStates = Component(EpicsSignalRO, "STATES-RBV", auto_monitor=True, kind=Kind.hinted)
@ -239,8 +247,11 @@ class aa1Tasks(Device):
self._textToExecute = None self._textToExecute = None
else: else:
raise RuntimeError("Unsupported filename-text combo") raise RuntimeError("Unsupported filename-text combo")
self._isConfigured = True
if self._failure.value:
raise RuntimeError("Failed to launch task, please check the Aerotech IOC")
self._isConfigured = True
new = self.read_configuration() new = self.read_configuration()
return (old, new) return (old, new)
@ -455,7 +466,7 @@ class aa1DataAcquisition(Device):
return data return data
# DAQ data readback # DAQ data readback
data_rb = Component(EpicsSignalPassive, "DATA", kind=Kind.hinted) data_rb = Component(EpicsPassiveRO, "DATA", kind=Kind.hinted)
data_rows = Component(EpicsSignalRO, "DATA_ROWS", auto_monitor=True, kind=Kind.hinted) data_rows = Component(EpicsSignalRO, "DATA_ROWS", auto_monitor=True, kind=Kind.hinted)
data_cols = Component(EpicsSignalRO, "DATA_COLS", auto_monitor=True, kind=Kind.hinted) data_cols = Component(EpicsSignalRO, "DATA_COLS", auto_monitor=True, kind=Kind.hinted)
data_stat = Component(EpicsSignalRO, "DATA_AVG", auto_monitor=True, kind=Kind.hinted) data_stat = Component(EpicsSignalRO, "DATA_AVG", auto_monitor=True, kind=Kind.hinted)

View File

@ -0,0 +1,167 @@
## Integration log for the Ophyd integration for the Aerotech Automation1 EPICS IOC
## Avoid the safespace API!!!
The properly documented beamline scripting interface was meant for exernal users. Hence, it is idiotproof and only offers a very limited functionality, making it completely unsuitable for serious development. Anyhing more complicated should go through the undocumented scan API under 'bec/scan_server/scan_plugins'. The interface is a bit inconcvenient and there's no documentation, but there are sufficient code examples to get going, but it's quite picky about some small details (that are not documented). Note that the scan plugins will probably migrate to beamline repositories in the future.
## Differences to vanilla Bluesky
Unfortunately the BEC is not 100% compatible with Bluesky, thus some changes are also required from the ophyd layer.
### Event model
The BEC has it's own event model, that's different from vanilla Bluesky. Particularly every standard scan is framed between **stage --> ... --> complete --> unstage**. So:
- **Bluesky stepper**: configure --> stage --> Nx(trigger+read) --> unstage
- **Bluesky flyer**: configure --> kickoff --> complete --> collect
- **BEC stepper**: stage --> configure --> Nx(trigger+ read) --> complete --> unstage
- **BEC flyer**: stage --> configure --> kickoff --> complete --> complete --> unstage
What's more is that unless it's explicitly specified in the scan, **ALL** ophyd devices (even listeners) get staged and unstaged for every scan. This either makes device management mandatory or raises the need to explicitly prevent this in custom scans.
### Scan server hangs
Unfortunately a common behavior.
### Class
The DeviceServer's instantiates the ophyd devices from a dictionary. Therefore all arguments of '__init__(self, ...)' must be explicity named, you can't use the shorthand '__init__(self, *args, **kwargs)'.
'''python
# Wrong example
#class aa1Controller(Device):
# def __init__(self, *args, **kwargs):
# super().__init__(*args, **kwargs)
# self._foo = "bar"
# Right example
class aa1Controller(Device):
def __init__(self, prefix="", *, name, kind=None, read_attrs=None, configuration_attrs=None, parent=None, **kwargs):
super().__init__(prefix=prefix, name=name, kind=kind, read_attrs=read_attrs, configuration_attrs=configuration_attrs, parent=parent, **kwargs)
self._foo = "bar"
'''
### Status objects (futures)
This is a major time sink. Bluesky usually just calls 'StatusBase.wait()', and it doesn't even check the type, i.e. it works with anything that resembles an ophyd future. However the BEC adds it's own subscription that has some inconveniences...
#### DeviceStatus must have a device
Since the BEC wants to subscribe to it, it needs a device to subscribe.
'''python
# Wrong example
# def complete(self):
# status = Status()
# return status
# Right example
def complete(self):
status = Status(self)
return status
'''
#### The device of the status shouldn't be a dynamically created object
For some reason it can't be a dynamically created ophyd object.
'''python
# Wrong example
# def complete(self):
# self.mon = EpicsSignal("PV-TO-MONITOR")
# status = SubscriptionStatus(self.mon, self._mon_cb, ...)
# return status
# Right example
def complete(self):
status = SubscriptionStatus(self.mon, self._mon_cb, ...)
return status
'''
### Scans
Important to know that 'ScanBase.num_pos' must be set to 1 at the end of the scan. Otherwise the bec_client will just hang.
'''python
class AeroScriptedSequence(FlyScanBase):
def __init__(self, *args, parameter: dict = None, **kwargs):
super().__init__(parameter=parameter, **kwargs)
self.num_pos = 0
def cleanup(self):
self.num_pos = 1
return super().cleanup()
'''
Note that is often set from a device progress that requires additional capability from the Ophyd device to report it's current progress.
'''
class ProggressMotor(EpicsMotor):
SUB_PROGRESS = "progress"
def __init__(self, prefix="", *, name, kind=None, read_attrs=None, configuration_attrs=None, parent=None, **kwargs):
super().__init__(prefix=prefix, name=name, kind=kind, read_attrs=read_attrs, configuration_attrs=configuration_attrs, parent=parent, **kwargs)
self.subscribe(self._progress_update, run=False)
def _progress_update(self, value, **kwargs) -> None:
"""Progress update on the scan"""
if not self.moving:
self._run_subs( sub_type="progress", value=1, max_value=1, done=1, )
else:
progress = np.abs( (value-self._startPosition)/(self._targetPosition-self._startPosition) )
self._run_subs(sub_type="progress", value=progress, max_value=1, done=np.isclose(1, progress, 1e-3) )
'''
calculated
Otherwise