Closed egaltier closed 4 years ago
File ns_timing.py from old mecpython:
import pypsepics
from numpy import *
from time import sleep
from utilities import estr
class NS_Timing(object):
''' Class that defines a timing channel of a DG645 delay generator.
It uses an epics PV that is the value of the offset, so that timings
can be given with respect to a reference. For example, the ns laser
timing would be specified with respect to a t0, which is when the
of the laser and the x-rays coincide.
Since most users think of delaying the X-rays with respect to the
laser, positive delays pull the laser trigger earlier.
So:
The offset is the value on the DG645 box that corresponds to zero delay
The delay = offset - Value_on_DG645
The delay is also written to an epics pv whenever it is written or read
'''
def __init__(self,channel_pvbase='MEC:LAS:DDG:03:e',high_lim=1.0,low_lim=0.0,offset_pv_name='MEC:NOTE:LAS:NST0',delay_pv_name=None,name='nstiming'):
self._name=name
self._pvbase=channel_pvbase
self._low_lim=low_lim
self._high_lim=high_lim
self._offset_pv_name=offset_pv_name
self._delay_pv_name=delay_pv_name #epics variable that hold the current delay. It is updated whenever a delay is written or read by .delay()
def __call__(self,value=None):
'''if instance is called with no atribute, it gives back or put the value inthe delay:
usage: nstiming() : reads back current delay
nstiming(value): puts value in delay
'''
return self.delay(value)
def get_offset(self):
'''returns the value of the offset'''
return pypsepics.get(self._offset_pv_name)
def set_offset(self,value):
'''writes the value to the PV that holds the offset value'''
pypsepics.put(self._offset_pv_name,value)
def wait(self):
'''waits 0.1s, such that the DG645 is updated before continuing the script'''
sleep(0.1)
def dial_delay(self,value=None):
'''return the current delay if value is None.
If a value is passed, the delay is change to that value. All values in seconds.
The returned values are those directly read of the DG645, without offset taken into
account.
'''
if value==None:
delay_read=pypsepics.get(self._pvbase+'DelaySI')
return double(delay_read[5:])
elif self._low_lim < value < self._high_lim:
pypsepics.put(self._pvbase+'DelayAO',value)
else: print('Delay outside of allowed range')
def delay(self,value=None):
'''return the current delay of the X-rays with respect to laser if value is None.
If a value is passed, the delay is change to that value. All values in seconds.
'''
if value==None:
delay_value=self.get_offset() - self.dial_delay()
if self._delay_pv_name!=None:
pypsepics.put(self._delay_pv_name,delay_value)
return delay_value
else:
self.dial_delay(self.get_offset() - value)
if self._delay_pv_name!=None:
pypsepics.put(self._delay_pv_name,value)
def status(self):
'''return a string that formats the delay and t0 '''
retstr='Nanosecond laser : '
delay=self.delay()
if not (-5e-9 < delay < 50e-9):
color='red'
type='bold'
else:
color='black'
type='normal'
retstr+=estr('Delay = ' + str(delay) +'\n',color=color,type=type)
retstr+=estr(' T0 = ' + str(self.get_offset()) +'\n',color=color,type=type)
return retstr
def redefine_delay(self,value=0.0):
'''redifine the current value of the delay generator as the delay given by
the value. The value is standard 0.
usage: redefine_delay(): the current setting of the delay generator correspond to t0
redefine_delay(4e-9): the current setting of the delay generator correspond to a delay of 4ns
'''
self.set_offset(self.dial_delay()+value)
def mvr(self,value):
"""Moves the delay 'value' reltive the current delay"""
old_delay=self.delay()
new_delay=old_delay+value
self.delay(new_delay)
File fs_timing from old mecpython:
import pypsepics
import KeyPress
import sys
from numpy import *
from time import sleep
from utilities import estr
class FS_Timing(object):
''' Class that allows changing the delay of the femto-second laser, and compersate a delay stage in the timetool to keep the signal there centered.
This class does NOT interact with the EPICS phase locking system. Timing should not be changed with that electronic delay once t0 is determined
for each experiment, except when larger delays are required that the stage can handle.
Keeping the timetool signal centered is accomplished by keeping the following relation between the timetool_motor and the delay_motor:
timetool_motor - delay_motor = _timetool_offset
general usage:
fstiming(1e-12) : set the delay to 1 ps, by moving the delay stage. Moves the delay stage on the timetool to keep signal stationary.
redefine_t0() : set the current value of the delay stage motor as t0. From now on relative delay are defined with respect to this refence.
redefine_timetool_t0() : run this when the timetool sygnal is centered in the white light spectrum. Set the time_tool_offset to the correct value.
'''
def __init__(self,delay_motor,high_lim=0.5e-9,low_lim=-0.5e-9,timetool_motor=None,delay_pv='MEC:NOTE:LAS:FSDELAY',offset_pv='MEC:NOTE:LAS:FST0',timetool_offset_pv='MEC:NOTE:DOUBLE:56',name='fstiming'):
self._name=name
self.delay_motor=delay_motor
self._low_lim=low_lim
self._high_lim=high_lim
self.timetool_motor=timetool_motor
self._delay_pv=delay_pv # PV only for monitoring purposes. Changing this value won't change the delay.
# its value is never read by python, only written to
self._offset_pv=offset_pv # PV for offset value of delay motor. when motor is put to this value, the delay is 0fs.
self._timetool_offset_pv=timetool_offset_pv # PV for the offset value of the timetool.The difference of the timetool motor and
# the delay motor should equal this value. This ensure there will be an edge centered
# in the timetool signal.
self._c=299792458.0 # speed of light is m/s
def __call__(self,value=None):
'''if instance is called with no atribute, it gives back or put the value inthe delay:
usage: nstiming() : reads back current delay
nstiming(value): puts value in delay
'''
return self.delay(value)
def __write_delay_to_pv(self,value):
''' Write tvalue to the PV of the delay
'''
pypsepics.put(self._delay_pv,value)
def __get_offset(self):
'''returns the value of the offset.
The offset is the motor value on the delay stage that will result in
time delay calculation of 0 (i.e. put the delay motor at the offset value and
the delay() command will return 0)
'''
return pypsepics.get(self._offset_pv)
def __set_offset(self,value):
'''Writes the value to the PV that holds the offset value.
The value is a motor position in mm.
The offset is the motor value on the delay stage that will result in
time delay calculation of 0 (i.e. put the delay motor at the offset value and
the delay() command will return 0)
'''
pypsepics.put(self._offset_pv,value)
def redefine_t0(self):
'''Redefines the current position of the delay motor as t0.
usage : fstiming.redefine_t0()
'''
self.__set_offset(self.delay_motor.wm())
self.__write_delay_to_pv(0.0)
def __get_tt_offset(self):
''' Reads the offset from the timetool pv '''
return pypsepics.get(self._timetool_offset_pv)
def __set_tt_offset(self,value):
''' write value to the offset from the timetool pv '''
return pypsepics.put(self._timetool_offset_pv,value)
def redefine_timetool_t0(self):
''' Set the timetool offset to current value.
This command should be run when the edge of the timetool is in the center of the spectrum.
Subsequent motions of the timing with delay(), will then keep the timetool signal centered,
since delay will alway keep the relation timetool_motor - delay_motor = timetool_offset
'''
new_offset=self.timetool_motor.wm() - self.delay_motor.wm()
self.__set_tt_offset(new_offset)
def delay(self,value=None):
'''Returns or changes the delay.
If no value is passed, the current delay is calculated and returned.
If a value is passed, the delay is changed to that value.
'''
if value==None:
dt_current=-2*(self.delay_motor.wm()-self.__get_offset())/1000.0/self._c #factor of thousand since the motor values are in mm, not m
self.__write_delay_to_pv(dt_current)
return dt_current
elif self._low_lim < value < self._high_lim:
delay_motor_value=-(value*self._c*1000.0/2)+self.__get_offset()
self.delay_motor.mv(delay_motor_value) # set delay_motor to correct position for the delay
self.timetool_motor.mv(delay_motor_value+self.__get_tt_offset()) # adjust timetool motor for correct delay
self.delay_motor.wait()
self.timetool_motor.wait()
self.__write_delay_to_pv(value) # waits for both motors to stop moving.
else: print 'delay outside of allowed range'
def mvr(self,value=0):
''' changes the delay a relative mount of value'''
old_delay=self.delay()
new_delay=old_delay+value
self.delay(new_delay)
def status(self):
'''return a string that formats the delay and t0 '''
retstr='Femtosecond laser : '
delay=self.delay()
retstr+=estr('Delay = ' + str(delay) +'\n',color='black',type='normal')
return retstr
def tweak(self,step=0.1e-12,dir=1):
help = "q = exit; up = step*2; down = step/2, left = neg dir, right = pos dir\n"
help = help + "g = go to abs timing"
print "tweaking the delay"
#print "tweaking motor %s (pv=%s)" % (motor.name,motor.pvname)
print "current delay :"+ str(self.delay())
if dir != 1 and dir != -1:
print("direction needs to be +1 or -1. setting dir to 1")
step = float(step)
oldstep = 0
k=KeyPress.KeyPress()
while (k.isq() is False):
if (oldstep != step):
print "stepsize: %f" % step
sys.stdout.flush()
oldstep = step
k.waitkey()
if ( k.isu() ):
step = step*2.
elif ( k.isd() ):
step = step/2.
elif ( k.isr() ):
self.mvr(step)
print self.status()
elif ( k.isl() ):
self.mvr(-step)
print self.status()
elif ( k.iskey("g") ):
print "enter absolute position (char to abort go to)"
sys.stdout.flush()
v=sys.stdin.readline()
try:
v = float(v.strip())
self.delay(v)
except:
print "value cannot be converted to float, exit go to mode ..."
sys.stdout.flush()
elif ( k.isq() ):
break
else:
print help
print self.status()
This request is linked to the MEC LV80 beamtime and is quite urgent since the checkout beamtime would be next thursday (09/17). Hopefully, the posted codes (ns_timing.py
and fs_timing.py
) from the old python version can help to develop the new code.
@egaltier Yes, I remember this request now. Sorry; it dropped off my radar. I'll take a look.
@egaltier Looking over this, I think this will be fairly simple. What I see here is a python class that takes a variable delay value and applies it to a group of DG channels; in the case of the LPL we would give the class a group of 4 channels, and associated notepad PVs for recording T0. For the SPL it's even simpler; you're just manipulating the Vitara PV.
Do you have more PVs we can use for recording the T0 value for each delay? I only see one notepad PV in the old NS timing, which doesn't seem adequate for this. One PV seems OK for the FSL.
I also notice that you didn't mention anything about the timetool, though there is functionality in the old fs_timing.py for the timetool. Is this not needed for this experiment?
The previous way that the various components of the LPL were being triggered had them slaved to a single channel on the master SRS box. The recent changes have removed this with all the channels of the SRS box being slaved directly to the EVR. It means that all the 4 channels of the SRS box has to be moved simultaneously instead of only one like before. I'm pretty sure we have more available PV variables to store those additional channels.
As for the time tool, indeed, this is not needed for the upcoming experiment. It will be necessary for the next beamtime LV25. I wanted to first have our meeting on the timetool analysis script before discussing the script for the time delay.
@egaltier I quickly roughed out a class that I think does what you want. The LPL will build on this by using 4 of these in a higher level object, with some methods that simply call the same methods on all 4 channels.
Here it is. Let me know what you think.
class TimingChannel(object): def init(self, setpoint_PV, readback_PV, name): self.control_PV = FCpt(EpicsSignal, setpoint_PV) # DG/Vitara PV self.storage_PV = FCpt(EpicsSignal, readback_PV) # Notepad PV self.name = name
def set_t0(self, val=None): """ Set the t0 value directly, or save the current value as t0. """ if not val: # Take current value to be t0 if not provided val = self.control_PV.get() self.storage_PV.put(val) # Save t0 value
def mvr(self, relval):
"""
Move the control PV relative to it's current value.
"""
currval = self.control_pv.get()
self.control_pv.put(currval + relval)
def get_delay(self, verbose=False):
delay = self.control_PV.get() - self.storage_PV.get()
if delay < 0:
print("X-rays arrive {} s before the optical laser".format(delay))
elif delay > 0:
print("X-rays arrive {} s after the optical laser".format(delay))
else: # delay is 0
print("X-rays arrive at the same time as the optical laser")
if verbose:
control_data = (self.name, self.control_pv.prefix,
self.control_pv.get())
storage_data = (self.name, self.storage_pv.prefix,
self.storage_pv.get())
print("{} Control PV: {}, Control Value: {}".format(*control_data))
print("{} Storage PV: {}, Storage Value: {}".format(*storage_data))
It looks good! Just a note: for the LPL, t0
will be set by each 4 channels, not just one. If at least one value is different than the saved t0
while the others are correct then something is wrong and needs to be notified since these values control the amplification of the laser beam.
@egaltier Yes, in the higher level version for the LPL we will loop over all 4 channels, applying the same relative delay to each.
e.g. something like:
class NSTiming(object):
channels = [TimingChannel(...),
TimingChannel(...),
....]
def set_t0(self, val):
for channel in channels:
channel.set_t0(val)
It sounds like you want to have some level of checking as well? I'm not sure how we can make sure that someone has not modified the PV Notepad values outside of this class, since the code is using this as our source of truth. The code doesn't save t0 values within the class; that's what the PVs are for.
These 4 channel values are controlled by the laser people and essentially, should always be our t0
. While they do not care about the 'absolute' EVR trigger input value, the whole laser is slaved to it and its perfomrance depends on it. SO I would suggest that the current values are saved as 4 laser PVs (maybe with a function called lpl_save_master_timing()
or something like that. This would basically be a backup of the timing in case somebody change is inadvertantly using the commands we use for the timing. This is a precaution I think we need since this time we don't have a single channel to change the whole timing from but rather 4 channels slaved to the EVR itself. Then, out time delay functions would act on the channels, and essentially, the save_t0
function is likely going to be a copy of the lpl_save_master_timing()
but pushing the values onto a different set of 4 PVs. Again, these are redundant but is important for the safety of the laser. I'm pretty sure that at some point, we would like to have this lpl_save_master_timing()
function changed into lpl_save_timing()
to actually save all the channels of all the SRS boxes used to create the laser so you have a backup of these important timing functions in a place you can easily recall. For now, they save the status of the boxes on memory channels of the SRS box themselves (but if the boxes changed, these values are lost) and on a paper kept in a google drive folder.
I agree, these values should be saved separately. In fact, while I was writing this code, it occurred to me that this functionality should actually be in an IOC because it would provide the redundancy that you want, as well as put everything in the archiver so you could recover from mistakes. However, I think that is a different development effort, and pen-and-paper (analog or digital) methods should be OK for now, since they have been working for years. :)
P.S.: I guess another alternative would be to just use up another 4 notepad PVs, and write a quick function to do this. I was planning on use MEC:NOTE:DOUBLE:41 through MEC:NOTE:DOUBLE:44 for saving the LPL channels; I could extend this to go through MEC:NOTE:DOUBLE:48 to have a second "backup" set. This backup set would only be affected by this lpl_save_master_timing() function (or somebody manually accessing the PV, but I can't do much about that, unless we have a separate IOC hint hint). :)
What do you think?
Getting 4 more PVs (up to MEC:NOTE:DOUBLE:48
) seems good enough to start with, yes! As for the IOC, you decide what is the best way: if you think an IOC is the best way, go for it. I would have a meeting with Eric C. before to see what could be done on a more global approach.
Feature Request
Define functions to delay the X-ray pulse vs an optical laser before interacting with a sample.
Existing Alternatives
I did not find any similar functions in the current mecpython version. The alternative is set several DG645 boxe's channels or VITARA target time manually, everytime that a different time is required. Incorrect values can be potentially set which could be detrimental to the laser systems (the VITARA could go out of range and pose issues for locking, bucket could be jumped for long pulse laser sysems, ...).
Context
While this request is linked to MEC, some of the functions are relevant for short pulse duration laser systems available in the other hutches. At MEC, we have a long pulse laser system whose delay vs the LCLS is achieved by changing delays on channels of an SRS box, and a short pulse laser system whose delay vs the LCLS is achieved by changing the target time of the VITARA. Changing timing in these devices is risky so controlling it by the use of a script helps to secure safe operation by the staff or the users. In the past, similar type of functions have been developed (old versions as
*.py
files are attached to this issue) but are now missing in the new hutch python implementation.Suggestions There should be two types of function implemented, as a function of the laser system in use:
where
spl
stands for short pulse laser (a short pulse duration is ps or fs) andlpl
stands for long pulse laser (a long pulse duration is ns). From the user perspective, it makes more sense to start naming these functions byxray_delay
since in a pump-probe experiment at an FEL, the FEL is usually used as a probe with the optical laser used to excite the sample (whether it is used in bio, condensed material or high energy density science). Of course, we know that the X-ray arrival time on sample cannot be changed and that we actually change the optical laser timing. But in a python implementation, this should be transparent to users to avoid confusion.Each function should have a set of common attribute like:
Each of these attributes are important for the operation, here are their description and common use:
delay is
negative
the X-rays arrive before the optical laser. So, if one enteredxray_delay_vs_spl(300e-15)
, the delay text could read:def set_t0(self, val):
def mvr(self):
def xray_delay_vs_spl(self, val):