pcdshub / mec

1 stars 6 forks source link

Time delay between X-ray and optical lasers #7

Closed egaltier closed 4 years ago

egaltier commented 4 years ago

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:

> xray_delay_vs_spl
> xray_delay_vs_lpl

where spl stands for short pulse laser (a short pulse duration is ps or fs) and lplstands for long pulse laser (a long pulse duration is ns). From the user perspective, it makes more sense to start naming these functions by xray_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:

> xray_delay_vs_spl.get_delay() or xray_delay_vs_spl.status()
> xray_delay_vs_spl.set_t0()
> xrat_delay_vs_spl.mvr()

Each of these attributes are important for the operation, here are their description and common use:

def get_delay(self, verbose=False):
 For the short pulse laser (xray_delay_vs_spl), print the current delay like:
 > X-rays are delayed by 300 fs with respect to the short pulse laser
 If verbose is True, display more information concerning the actual value of the VITARA target time.
 For the short pulse laser (xray_delay_vs_lpl), print the current delay like:
 > X-rays are delayed by 5 ns from the long pulse laser
 If verbose is True, display more information concerning the actual values of the channels in the SRS box.
 Additionally, if a delay is `positive`, it means that the X-rays arrive **after** the optical laser, while when the

delay is negative the X-rays arrive before the optical laser. So, if one entered xray_delay_vs_spl(300e-15), the delay text could read:

X-rays arrive 300 fs after the short pulse laser. while, if xray_delay_vs_spl(-300e-15), the delay text could read: X-rays arrive 300 fs before the short pulse laser. The after and before could have a bold type of some specific colors as a plus.


code

def set_t0(self, val):

     Extract the current values of the VITARA target time or the various relevant channels in the SRS box or use 
the **absolute** value and use it/them as a relative time overlap (`t0`) between the X-rays and the optical laser.
 The values could be stored in a PV or whatever makes sense to allow for easy access later. On the VITARA 
output example attached to this issue, the target value is pushed to 1020.57020 ns:
     > xray_delay_vs_spl.set_t0(1020.57020e-9) # or
     > xray_delay_vs_spl.set_t0()
 code

def mvr(self):

     Change the delay **relatively** vs the current delay. The current delay could be `t0` or any other delay. The 
value can be positive or negative, with units in `seconds` so that probing 100 fs later than the current delay 
value would be:
     > xray_delay_vs_spl.mvr(100e-15) # this would remove 100fs to the VITARA target time
     > xray_delay_vs_npl.mvr(10e-9) # this would remove 10e-9 to a specific channel in a SRS box
     > xray_delay_vs_npl.mvr(-2e-9) # this would add 2e-9 to a specific channel in a SRS box
 code

def xray_delay_vs_spl(self, val):

     Change the delay between the X-ray and the optical laser, setting the actual experimental value rather than 
the electronic value. It does change the delay vs the `t0` set earlier. Essentially, it would 'push' the `t0` - `val` 
directly to a channel/target field. The value can be positive or negative, with units in `seconds`. On the VITARA 
example attached to the issue, if a user wants to send the X-rays 300 fs later vs the optical laser, the command
 would read:
     > xray_delay_vs_spl(300e-15) # the value pushed to the VITARA would be 1020.57020e-9 - 300e-15 = 
1020.56990e-9, which will effectively trigger the optica laser 300 fs before the X-rays.
 code


**MEC Implementation**
The various relevant parameters for MEC are the following:
- for the lpl: the SRS DG box MEC:LAS:DDG:03, channel A, C, E and G needs to be changed. So, for example, if 
the values shown in the file `dg_lpl.png` are the `t0`for the lpl, then using `xray_delay_vs_lpl.set_t0()` would store the values:
  - `0.000875848` (chA), 
  - `0.0000689354` (chC),
  - `0.0000689354` (chE),
  - `0.00010184933` (chG)
Then, using `xray_delay_vs_lpl.get_delay()` would produce something like:
  - `> X-rays are delayed by 0 ns from the long pulse laser`
And if the users want to probe with the X-ray 3.0 ns later than the optical laser, using `xray_delay_vs_lpl(3.0e-9)` would set the values of the SRS DG box MEC:LAS:DDG:03 to:
  - `0.000875845` (chA), 
  - `0.0000689324` (chC),
  - `0.0000689324` (chE),
  - `0.00010184633` (chG)
Finally, using `xray_delay_vs_lpl.get_delay()` would produce something like:
  - `> X-rays are delayed by 3.0 ns from the long pulse laser` #or
  - `> X-rays arrive 3.0 ns after the long pulse laser`
- for the spl: the VITARA (LAS:FS6:VIT) target channel is the only value to change. Using `xray_delay_vs_spl.set_t0()` would store `1020.57020e-9` in a PV or something like that. Then, using `xray_delay_vs_lpl.get_delay()` would produce something like:
  - `> X-rays are delayed by 0 fs from the short pulse laser`
And if the users want to probe with the X-ray 500.0 fs later than the optical laser, using `xray_delay_vs_spl(500.0e-15)` would set the target value of the VITARA (LAS:FS6:VIT) to `1020.569700e-9 ` and `xray_delay_vs_spl.get_delay()` would produce something like:
  - `> X-rays are delayed by 500.0 fs from the short pulse laser` #or
  - `> X-rays arrive 500.0 fs after the short pulse laser`.

<img width="1440" alt="dg_lpl" src="https://user-images.githubusercontent.com/44449282/92986070-12aa8880-f46d-11ea-900a-7f9832ae05b1.png">
<img width="1119" alt="vitara" src="https://user-images.githubusercontent.com/44449282/92986073-163e0f80-f46d-11ea-8562-00e5cc15a181.png">
egaltier commented 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)
egaltier commented 4 years ago

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()
egaltier commented 4 years ago

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.

slactjohnson commented 4 years ago

@egaltier Yes, I remember this request now. Sorry; it dropped off my radar. I'll take a look.

slactjohnson commented 4 years ago

@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?

egaltier commented 4 years ago

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.

slactjohnson commented 4 years ago

@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))
egaltier commented 4 years ago

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.

slactjohnson commented 4 years ago

@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.

egaltier commented 4 years ago

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.

slactjohnson commented 4 years ago

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?

egaltier commented 4 years ago

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.