Source code for hardware_interfaces.pi_control

# Import helper modules
from typing import List, Tuple
import logging
import os

# Import from our custom modules
from data_processing import find_wavegen_params

# Import essential modules
from pipython import GCSDevice, pitools

# Set up logger
logger = logging.getLogger(__name__)


[docs]class PiController: """ This function initializes the Physik Instrumente (PI) controller. Warning: * Be sure that the controller and the stages are compatible, else you will get an error * Be sure that all controller are connected and detected (!) by the computer * Check in PIMikroMove) if necessary **The current controllers which are typically used** 1. I-Lab: * C-413 (USB or COM-Port connection) with stages: * V-522.1AA - Interferometer Stage * C-843 (PCI board connection) with stages: * M-415.PD - IR Delay Stage * M-531.PD - UV/VIS Delay Stage 2. H-Lab: * C-413 (USB or COM-Port connection) with stages: * V-522.1AA - Interferometer Stage * C-843 (PCI board connection) with stages: * M-415.PD - IR Delay Stage * M-406.6PD - UV/Vis Delay Stage Most information about the controllers and stages can be found in the respective data sheets or by opening PIMikroMove. PI also provides example files to understand the motor controll better. **Useful when implementing a new controller:** One useful command is "GCSCommands.funcs" which returns a list of available commands for the chosen stage. Upon switching a stage to a different controller, make sure all connections to controller are closed and turn off the power supply of the controller(s). Always check if the motor is supported by the respective controller which you want it to connect to. If it is necessary to connect a new controller via COM-port or TCPIP etc. please look into the documentation (mainly GSCDevice). Do not forget to edit the classes properly (i.e. add If statements for different controllers). Args: controller_name (str): The controller name which is defined by PI * E.g. "C-413" stage_names (List[str]): Name of the stage/ motor for each axis which is defined by PI * E.g. stage_names = ["M-406.6PD","M-531.PD1"] com_port (str, optional): Port to which the controller is connected to (if applicable). This number can be found when opening the program "PIMikroMove" and trying to connect the controller. Defaults to None. * E.g. "COM15" pci_board_number (int, optional): Board number which specifies the slot inside the computer where the PCI-Card is located (if applicable). Defaults to None. name (str, optional): Name / Identifier to give to this controller. This is relevant for log statements, especially when there is more than one Controller in the setup. Defaults to "PIControllerXY". Attributes: controller_name (str): The controller name which is defined by PI * E.g. "C-413" com_port (str): Port to which the controller is connected to (if applicable). This number can be found when opening the program "PIMikroMove" and trying to connect the controller. * E.g. "COM15" stage_names (List[str]): Name of the stage/ motor for each axis which is defined by PI * E.g. stage_names = ["M-406.6PD","M-531.PD1"] pci_board_number (int): Board number which specifies the slot inside the computer where the PCI-Card is located (if applicable). axes (List[int]): List containing the number of axis for the corresponding stages. axis_stage (dict of {int: str}): Dictionary with axes number as key and stage names as values. reference_mode (List[str]): Reference mode which is used for the stages. See inline comments for more details. pidevice (GCSDevice): GCSDevice object containing the functionalities of the PI Controller/ Device id_controller (str): ID of the controller. reference_state (bool): Returns (False) True if stage is (not) referenced. servo_state (bool): Returns (False) True if servo is turned (off) on. auto_zero_state (bool): Returns (False) True if stage is (not) auto-zeroed. min_range (int): Minimum position the stage can move to. max_range (int): Maximum position the stage can move to. init_velocity (float): Initial velocity after initialization. init_position (float): Initial position after initialization. References: * The necessary documentation (Python API) for PI-Hardware can be found in the PI Python Libraries provided by PI. * A filereferencing to the documentation can be found in the datasheets and installation folder which is privately provided. * There also exists a file called "gsccommands.py", "pitools.py" and "gcsdll.py" where every command is listed in great detail. It can be found in datasheets and installation folder which is privately provided. """ # todo explain where to find DLL # todo resolve WARNING:root:no GCSTranslator path in Windows registry # todo (HKLM\SOFTWARE\PI\GCSTranslator) def __init__( self, controller_name: str, stage_names: List[str], com_port: str = None, pci_board_number: int = None, name: str = "PIControllerXY", ): # Checks whether the necessary dll's provided by PI are in the # working directory working_dir = os.getcwd() if "PI_GCS2_DLL_x64.dll" not in os.listdir(working_dir): raise Exception("PI_GCS2_DLL_x64.dll not found in {}".format(working_dir)) elif "C843_GCS_DLL_x64.dll" not in os.listdir(working_dir): raise Exception("C843_GCS_DLL_x64.dll not found in {}".format(working_dir)) # Defining the input arguments for the controller self.name = name self.controller_name = controller_name self.stage_names = stage_names # Generate a list for all the axes which will be connected self.axes = list(axes for axes, stages in enumerate(self.stage_names, 1)) self.axis_stage = dict(zip(self.axes, self.stage_names)) # Set the reference mode for each stage to FRF. If there ist no # stage, set NONE FRF - Make the referencing at the reference # switch (typically somewhere in the middle) FNL - Make the # referencing at the negative limit switch FPL - Make the # referencing at the positive limit switch This setting depends # on the motor and controller which is used self.reference_mode = [] for stage_name in self.stage_names: if stage_name != None: self.reference_mode.append("FRF") else: self.reference_mode.append(None) # * First step for connecting computer to the controller self.pidevice = GCSDevice(self.controller_name) if com_port == None and pci_board_number == None: logger.error( """Either comport or pci board number for {} are not provided. Make sure to provide at least one of the two, corresponding to the controller connectivity""".format( self.name ) ) self.end() # todo raise exception # Initialize controller C-413 #! The dll from PI needs to be in the current working directory if self.controller_name == "C-413": self.com_port = int(com_port.replace("COM", "")) # * Second step for connecting to controller (via RS232) self.pidevice.ConnectRS232(comport=self.com_port, baudrate=115200) self.id_controller = self.pidevice.qIDN().strip() # Define controller ID logger.info( """Controller {} (name: {}) is connected.""".format( self.id_controller, self.name ) ) # * Initialize stages logger.info("""Initializing connected stages for {}...""".format(self.name)) # C-413 automatically detects (physically) connected stages # Therefore no "stages" argument required in startup pitools.startup(self.pidevice, refmode=self.reference_mode) self.check_if_stages_init() self.get_init_params() # Initialize controller C-843 #! The dll from PI needs to be in the current working directory elif self.controller_name == "C-843": self.pci_board_number = pci_board_number # * Second step for connecting to controller (via PCI-Board) self.pidevice.ConnectPciBoard(board=self.pci_board_number) self.id_controller = self.pidevice.qIDN().strip() # Define controller ID logger.info("""Controller {} is connected.""".format(self.id_controller)) # * Initialize stages logger.info("""Initializing connected stages...""") # C-843 does not automatically detects (physically) connected stages pitools.startup( self.pidevice, stages=self.stage_names, refmode=self.reference_mode ) self.check_if_stages_init() self.get_init_params()
[docs] def check_if_stages_init(self): """ Checks if stages/ axes are referenced. If applicable the function autozeroes the stages. Checks if servos are on. The stages used to this point ('V-522.1AA' for 'C-413' and 'M-531.PD1' for 'C-843' ) auto-reference themselves upon pistartup command. However, AutoZero has to be performed seperately. It is important to check if the stage which you use is able to auto-zero. The function also checks if the servos are on. """ # Check for every stage in stage_names if it is referenced/ # autozeroed Enumerate is used because the gcs-commands take # only the axes (int) as arguments Enumerate starts at 1, not at # 0 for axis, stage in enumerate(self.stage_names, 1): # Returns (False) True if stage is (not) referenced self.reference_state = self.pidevice.qFRF(axis) # Returns (False) True if servo is turned (off) on self.servo_state = self.pidevice.qSVO(axis) # Returns (False) True if stage is (not) auto-zeroed if "qATZ" in self.pidevice.funcs: self.autozero_state = self.pidevice.qATZ(axis) else: logger.info( """The stage "{}" does not have an auto-zero option""".format(stage) ) # If qFRF returned True everything is fine, else something # is wrong with the stage self.reference_state[axis] because # a odict with [axis, bool]. We are interested in the bool if self.reference_state[axis] == True: logger.info( """The stage "{}" was successfully referenced for {}.""".format( stage, self.name ) ) else: logger.error( """The stage "{}" was not successfully referenced for {}. Controller connection will be closed. Please check the connection and resolve the problem.""".format( stage, self.name ) ) self.end() # Check if servo has turned on properly if self.servo_state[axis] == True: logger.info( """The servo of stage "{}" is turned on for {}.""".format( stage, self.name ) ) else: logger.error( """Problem while turning on servo of stage "{}" for {}.""".format( stage, self.name ) ) # Start AutoZero procedure if stage is not autozeroed yet if ( "qATZ" in self.pidevice.funcs ): # some stages do not have an auto-zero procedure if self.autozero_state == False: logger.info("""Autozeroing process for stage {} started...""") # Start an appropriate calibration procedure for # 'axes'. The AutoZero procedure will move the axis, # and the motion may cover the whole travel range. # Make sure that it is safe for the stage to move. # lowvoltage corresponds to the force applied to # hold the stage at the reference point This is # typically only relevant when using the stage in # vertical applications self.pidevice.ATZ(axes=axis, lowvoltage=0) pitools.waitontarget(self.pidevice) # wait until autozero is done self.autozero_state = self.pidevice.qATZ(axis) if self.autozero_state == True: logger.info( """Autozero of {} was successfull for {}!""".format( stage, self.name ) ) else: logger.error( """Autozero of {} was not successfull for {}! Controller connection will be closed. Please check the connection and resolve the problem""".format( stage, self.name ) ) self.end() else: pass
[docs] def get_init_params(self): """ Set velocity to a very low value (0.1) as default. Reads out all necessary initial parameters after initialization (max/ min position, current position etc.) """ # Determine the minimal and maximal range which the stages can # move self.min_range = self.pidevice.qTMN() self.max_range = self.pidevice.qTMX() # Set the velocity to very low value for safety reasons for axis in self.axes: self.pidevice.VEL(axis, values=0.1) # Obtain initial velocity value self.init_velocity = list(self.pidevice.qVEL(self.axes).values()) # Obtain initial position value self.init_position = list(self.pidevice.qPOS(self.axes).values()) logger.info( """The initial velocity of axis {} is set to {} mm/s for {}.""".format( self.axes, self.init_velocity, self.name ) ) logger.info( """The initial position of axis {} mm is {} for {}""".format( self.axes, self.init_position, self.name ) ) # Check if wavegeneration is possible with this controller self.has_wavegen = self.pidevice.HasqWAV()
def __str__(self): information = """ Controller name: {} Name (self defined): {} Controller ID: {} Stage name: {} Connected axes: {} Axes referenced: {} Servos activated : {} Maximum position: {} mm Minimum position: {} mm Position: {} in mm Velocity: {} in mm/s Is moving: {} """.format( self.controller_name, self.name, self.id_controller, self.stage_names, self.axes, self.pidevice.qFRF(), self.pidevice.qSVO(), self.max_range, self.min_range, self.init_position, self.init_velocity, self.pidevice.IsMoving(), ) return information
[docs] def end(self): self.pidevice.CloseConnection()
def __enter__(self): return self def __exit__(self, type, value, traceback): self.end() # todo test if this makes no problems. Send command and then exit
[docs]class PiStage: """ Provides all functionalities needed to communicate with the stage. Initializes the necessary arguments for the control of the Pi stages. Converts millimeters to fs and vice versa for the purpose of setting the movement range etc. Possible to set velocity. Possible to calculate the frequency for waveform generation. Regular sweep movements and waveform movements are possible. Args: controller (PiController): Object which results from initiating the PI controller. During this initialization, the controller and stage are initiated, referenced etc. For further commands (like move motor etc.) it is necessary to pass this object to PiStage since all gcs commands need it to work properly. axis (str): Axis of the stage. E.g. controller "C-413" has axes "1" and "2" where motors can be connected. velocity (float): Initial velocity of the stage in millimeter/second. t_zero (float): Stage position where both pulses temporally overlapp on the detector. Unit in mm. direction (int): Either -1 or +1. This is an important parameter to define in which direction the stage delay is defined. I.e. +1 would mean that to increase delays the stage needs to move to "more positive" positions (i.e. from 2.1 mm to 2.4 mm). If the stage is turned around by 180 degrees, now it would be necessary to set direction to -1 since it has to move to "more negative" directions (i.e. from 1.2 mm to 0.8 mm). path_factor (int): Number of times the light pulse travels back and/or forth on the delay stage. This is relevant for the conversion of a femtosecond delay time into a corresponding distance in mm and vice versa. For standard delays stages with two mirrors this factor is 2. The two mirrors are used to reflect the pulse once back and once forth (1+1=2). Moving the stage a given distance will increase the beam path by double that amount. In the I-Lab the UV/VIS delay stage is setup such that beam is reflected twice back and forth (2+2=4). This is achieved by "re"reflecting the pulse back onto the stage at a different height. The appropriate path_factor for this stage is 4, because for a movement of a given distance on that stage the beam path changes by the quadruple the amount of this distance. name (str, optional): Name / Identifier to give to this stage. This is relevant for log statements, especially when there is more than one stage in the setup. Defaults to "XYZ Stage". Attributes: controller (PiController): Object which results from initiating the PI controller. During this initialization, the controller and stage are initiated, referenced etc. For further commands (like move motor etc.) it is necessary to pass this object to PiStage since all gcs commands need it to work properly. axis (str): Axis of the stage. E.g. controller "C-413" has axes "1" and "2" where motors can be connected. t_zero (float): Stage position where both pulses temporally overlapp on the detector. Unit in mm. name (str, optional): Name / Identifier to give to this stage. This is relevant for log statements, especially when there is more than one stage in the setup. Defaults to "XYZ Stage". t_zero_in_fs (float): Stage position where both pulses temporally overlapp on the detector. Unit in fs. direction (int): Either -1 or +1. This is an important parameter to define in which direction the stage delay is defined. I.e. +1 would mean that to increase delays the stage needs to move to "more positive" positions (i.e. from 2.1 mm to 2.4 mm). If the stage is turned around by 180 degrees, now it would be necessary to set direction to -1 since it has to move to "more negative" directions (i.e. from 1.2 mm to 0.8 mm). path_factor (int): Number of times the light pulse travels back and/or forth on the delay stage. This is relevant for the conversion of a femtosecond delay time into a corresponding distance in mm and vice versa. For standard delays stages with two mirrors this factor is 2. The two mirrors are used to reflect the pulse once back and once forth (1+1=2). Moving the stage a given distance will increase the beam path by double that amount. In the I-Lab the UV/VIS delay stage is setup such that beam is reflected twice back and forth (2+2=4). This is achieved by "re"reflecting the pulse back onto the stage at a different height. The appropriate path_factor for this stage is 4, because for a movement of a given distance on that stage the beam path changes by the quadruple the amount of this distance. position (float): Absolute position of stage in mm. position_fs (float): Position of stage relative to t_zero in fs. velocity (float): Velocity of the stage in millimeter/second. velocity_fs (float): Velocity of the stage in femtoseconds/second. max_range_in_fs (float): Maximum movement position in femtoseconds. min_range_in_fs (float): Minimum movement position in femtoseconds. frequency (float): Frequency in Hz with which the stage will repeat the waveform. This is needed as feedback to properly choose the arguments for the waveform-generation. start_pos (float): Start position of the circular motion. Typically at the value of the offset. Unit in mm. wave_generator (int): Gives the wavegenerator an "identification" number i.e. 1. wave_table (int): The controller has 8 spaces for wave tables which can be stored. We use the 1. coherence_time (float): Maximum time between the fixed pulse and the movable pulse. amplitude (float): Amplitude of the coherence time scan / wavegeneration (+ buffer) in mm. reset_position (float): The reset position is the position at which the interferometer counter should be reset to 0 for the circular motion. Unit in mm. overshoot_factor (float, optional): When using the wavegeneration functionality in combination with the interferometer counter it was observed that the actual amplitude (coherence time) decreases with an increase in frequency (velocity) of the wavegeneration and increases with an increase of the the speedupdown parameter. To counter this the overshoot_factor is used to increase the setup coherence time by its factor on both sides. Actual amplitude = coherence_time*(1+2*overshoot_factor) + buffer overshoot_mm (float): Distance in mm that the stage overshoots beyond the specified coherence time on both sides. See setup_coherence_time_scan for further information. speedupdown (float): Only exists for stages with active wavegeneration. Sets how round the curve becomes at minimal/maximal amplitude. Necessary so that the motor does not make hard stops at these endpoints. approx_frequency (float): Clostest frequency that can be reached by wavegeneration to the requested frequency. """
[docs] def __init__( self, controller: PiController, axis: str, t_zero: float, direction: int, path_factor: int, velocity: float, name: str = "XYZ Stage", ): """ Initializes the necessary arguments for the control of the Pi stages. """ # Initialize Attributes self.controller = controller self.pidevice = self.controller.pidevice self.axis = axis self.name = name self.t_zero = t_zero self.wavegen_running = False self.direction = direction self.path_factor = path_factor # Convert t_zero in fs to t_zero in mm self.t_zero_in_fs = self.__convert_mm_to_fs(mm=self.t_zero) # Define max and min range of the stage in fs so that user # understands what he is allowed to put in self.max_range_in_fs = self.__convert_mm_to_fs( self.controller.max_range[self.axis] ) self.min_range_in_fs = self.__convert_mm_to_fs( self.controller.min_range[self.axis] ) # Set velocity self.set_velocity_mm(velocity) # Move to t zero where pulses overlap temporally self.move(0, wait=True)
[docs] def wait(self): """ Wait for stage to reach target position. """ logger.info( """Waiting until target position is reached for stage {}.""".format( self.name ) ) pitools.waitontarget(self.pidevice)
def __convert_fs_to_mm(self, femtoseconds: float): """ Converts femtoseconds to millimeters. Note: Because the beam gets reflected by two mirrors on the stage, a movement of the stage results in twice the distance for the light to travel (therefore the factor 2). This is accounted for in this function. Args: femtoseconds (float): Value in femtoseconds. Returns: float: Value in millimeters. """ speed_of_light = 299792458 # in m/s # Moving the stage a given distance can lead to changing the # beam path length by a multiple of this this distance. This # depends on how the optics are setup. We use the path factor to # account for this. mm = femtoseconds * 1e-12 * speed_of_light / self.path_factor return mm def __convert_mm_to_fs(self, mm: float): """ Converts femtoseconds to millimeters. Note: Because the beam gets reflected by two mirrors on the stage, a movement of the stage results in twice the distance for the light to travel (therefore the factor 2). This is accounted for in this function. Args: mm (float): Value in millimeters. Returns: float: Value in femtoseconds. """ speed_of_light = 299792458 # in m/s # Moving the stage a given distance can lead to changing the # beam path length by a multiple of this this distance. This # depends on how the optics are setup. We use the path factor to # account for this. femtoseconds = (self.path_factor * mm * 1e12) / speed_of_light return femtoseconds def __relative_position_in_fs(self): relative_position_mm = self.direction * (self.position - self.t_zero) self.position_fs = self.__convert_mm_to_fs(relative_position_mm)
[docs] def set_velocity(self, velocity: float): """ Sets velocity of the stage in femtoseconds/ second and optionally returns it. Args: velocity (float): Value of the velocity [femtoseconds/ second] at which the stage should move. If stage velocity is exceeded the program returns an error. """ velocity_in_mm = self.__convert_fs_to_mm(velocity) self.set_velocity_mm(velocity_in_mm) logger.info( """Velocity of stage {} is set to {} fs/s.""".format(self.name, velocity) )
[docs] def set_velocity_mm(self, velocity: float): """ Sets velocity of the stage and optionally returns it. Args: velocity (float): Value of the velocity [millimeters/ second] at which the stage should move. If stage velocity is exceeded the program returns an error. """ # Obtain velocity prior and after the change and set the new # velocity prior_velocity = self.pidevice.qVEL(axes=self.axis) # Get prior velocity value self.pidevice.VEL(axes=self.axis, values=velocity) # Set new velocity self.velocity = self.pidevice.qVEL(axes=self.axis)[ self.axis ] # Get new velocity value self.velocity_fs = self.__convert_mm_to_fs(self.velocity) logger.info( """Velocity of stage {} before the change was [axes, velocity:] {} mm/s.""".format( self.name, prior_velocity ) ) logger.info( """Velocity of stage {} is set to {} mm/s.""".format( self.name, self.velocity ) )
[docs] def set_t_zero(self, t_zero: float): """ Set a defined t_zero in mm. The stage moves relative to the t_zero position with the move methods, or the wavegen method Args: t_zero (float): Stage position where both pulses temporally overlapp on the detector. Unit in mm. """ # Update the attributes self.t_zero = t_zero # Convert into fs self.t_zero_in_fs = self.__convert_mm_to_fs(mm=self.t_zero) logger.info( """Reset temporal overlap (t_zero) of {} to {}.""".format( self.name, self.t_zero ) ) # Update relative position attribute self.__relative_position_in_fs()
[docs] def move_mm(self, position: float, wait: bool = True): """ Moves specified stage to a predefined position (in mm). Waits between moving if instructed. Args: position (float): Position in mm to which the stage should be moved. wait (bool, optional): Determines wheather the program waits until stage is on target/ reached the given position. Defaults to True. """ # Check whether the position is within bounds if position > self.controller.max_range[self.axis]: position = self.controller.max_range[self.axis] logger.warning( """Position out of bound (upper). Set to maximal position {} mm.""".format( self.controller.max_range ) ) logger.warning( """Maximum position for motor {} is {} mm.""".format( self.name, self.controller.max_range ) ) logger.warning( """Minimum position for motor {} is {} mm.""".format( self.name, self.controller.min_range ) ) elif position < self.controller.min_range[self.axis]: position = self.controller.min_range[self.axis] logger.warning( """Position out of bound (lower). Set to minimal position {} mm.""".format( self.controller.min_range ) ) logger.warning( """Maximum position for motor {} is {} mm.""".format( self.name, self.controller.max_range ) ) logger.warning( """Minimum position for motor {} is {} mm.""".format( self.name, self.controller.min_range ) ) # Obtain position before and after moving the stage logger.info("""Stage {} is moving to {} mm.""".format(self.name, position)) self.pidevice.MOV(axes=self.axis, values=position) # Wait on target if wait is true if wait == True: self.wait() self.position = self.pidevice.qPOS(axes=self.axis)[self.axis] self.__relative_position_in_fs()
[docs] def move(self, position: float, wait: bool = True): """ Moves specified stage to a predefined position (in fs) relative to t_zero position. If instructed, waits for movement to complete by halting the interpreter. Args: position (float): Position in fs to which the stage should be moved. wait (bool, optional): Determines wheather the program waits until stage is on target/ reached the given position. Defaults to True. """ # Convert fs values to corresponding mm with premade function position_in_mm = self.__convert_fs_to_mm(femtoseconds=position) # To determine the absolute target position (move_to_position) # it is necessary to take the directionality of the stage into # account. move_to_position = self.t_zero + (self.direction * position_in_mm) # Move motor to new position self.move_mm(position=move_to_position, wait=wait)
[docs] def calculate_frequency(self, number_of_points: int, tablerate: int): """ Calculates the frequency with which the stage will repeat the waveform. This is needed as feedback to properly choose the arguments for the waveform-generation. Output duration = servo cycle time * output rate * number of points The "servo cycle time" (max: 200µs, min: 100 µs) is the time which the servo needs to go from one data point to the next. The "output rate" (also called tablerate) basically increases the servo cycle time per data point. Make sure that the resulting frequency is not a multiple of the laser frequency. We want to get a laser shot for every bin (size of bin = half of the wavelengths of a HeNe lasers) of the interferometer. Therefore it is important to have "no harmonic overlap" between the laser shots and the stagefrequency. References: C-413 user manual provided by PI. Chapter 8.6 Wave Generator (p.151-176, especially p. 167) Args: number_of_points (int): Number of points for period of the wave. tablerate (int): Duration of a wave table point in multiples of servo cycle times (also see extended summary). Returns: float: frequency of wavegeneration cycles in Hz """ self.frequency = 1 / (200e-6 * tablerate * number_of_points) return self.frequency
[docs] def init_wavegen( self, number_of_points: int, amplitude: float, tablerate: int, offset: float, firstpoint: float, speedupdown: float, number_of_cycles: int = 1, ): """ Configure a waveform (here: RAMP waveform) and make the stage move in trajectory of this waveform. Warning: ! Before calling the waveform function make sure to check ! wheather your function arguments return a reasonable frequency ! (see calculate_frequency function). The RAMP waveform is defined by the arguments below. To experiment with possible waveforms and to see them in action it is advisable to open PI-Mikromove and use the waveform-generator. There it is also possible to obtain a preview of the waveform depending on the given parameters. References: | C-413 user manual provided by PI. Chapter 8.6 Wave Generator (p.151-176, especially p. 167). | gcscommands.py file. | Sample file wavegenerator_circle.py Args: number_of_points (int): Number of points for one wave period. Differently: Length of the single scan line curve (PI documentation). Ranges from 0 to 4096. start_pos (float): Start position of the circular motion in mm. For our purposes this would typically be the t_zero position (in mm). E.g. -10 as the lowest position of stage V-522.1AA. amplitude (float): Amplitude of the circular motion mm. E.g. Amplitude = 20 with offset of -10 means that the upper (lower) bound of the motion is 10 (-10). number_of_cycles (int, optional): Number of times in which the wavepattern is repeated. ! Set equal to 0 so that the motor repeats the wave ! indefinitely. Generator must be stopped ! "manually" with ! the stop_generator function then. Defaults to 1 (1 cycle). tablerate (int): The table rate (also called "output rate") increases the servo cycle time per data point. Duration of a wave table point in multiples of servo cycle times. See also function calculate_frequency. offset (float): Offset in mm of the scan. This value offsets the wave on the amplitude axis. See "amplitude" argument for an example. firstpoint (float): Index of the segment starting point in the wave table. Lowest possible value is 1. Typically the first datapoint in the wavetable is used. speedupdown (float): Sets how round the curve becomes at minimal/maximal amplitude. Necessary so that the motor does not make hard stops at these endpoints. """ #! If "firstpoint" is ever changed from 1 change start_pos #! accordingly! self.start_pos = offset # Start position of the circular motion. Typically at the value of the offset. self.wave_generator = 1 # Give the wavegenerator an "identification" number self.wave_table = 1 # The controller has 8 spaces for wave tables which can be stored. We use the 1. logger.info( """define RAMP waveform for wave table {} for {}.""".format( self.wave_table, self.name ) ) # WAV_RAMP creates a RAMP Waveform with the given arguments. #SINE, COSINE etc. are also possible #! If "firstpoint" is ever changed from 1 change start_pos #! accordingly! self.pidevice.WAV_RAMP( table=self.wave_table, firstpoint=firstpoint, numpoints=number_of_points, append="X", center=number_of_points / 2, speedupdown=speedupdown, amplitude=amplitude, offset=offset, seglength=number_of_points, ) # Wait until the controller is finished with writing the wave # table into the memory pitools.waitonready(self.pidevice) # Connect wave generator to wave table in memory: Wave table # selection. if self.pidevice.HasWSL(): logger.info( """Connect wave generators {} to wave tables {} for {}.""".format( self.wave_generator, self.wave_table, self.name ) ) self.pidevice.WSL(self.wave_generator, self.wave_table) # Set the number of cycles for wave generator output if self.pidevice.HasWGC(): logger.info( """Set wave generators {} to run for {} cycles for {}.""".format( self.wave_generator, number_of_cycles, self.name ) ) self.pidevice.WGC(self.wave_generator, number_of_cycles) # Set wave generator table rate. Necessary for proper frequency if self.pidevice.HasWTR(): logger.info( """Set wave table rate to {} for wave generators {} for {}.""".format( tablerate, self.wave_generator, self.name ) ) self.pidevice.WTR( self.wave_generator, tablerate, interpol=0 ) # interpol = 0 because C-413 does not support interpolation
[docs] def start_wavegen(self): """ Move to starting position (start_pos) and start wave generation movement. """ logger.info( """Move motor {} to their start position {} .""".format( self.name, self.start_pos ) ) self.move_mm(position=self.start_pos) logger.info( """Start wave generators {} for {}.""".format( self.wave_generator, self.name ) ) self.pidevice.WGO( self.wave_generator, mode=1 ) # mode = 1 start wavegeneration immediately self.wavegen_running = True
[docs] def stop_wavegen(self): """ Stop the wave generation movement. """ self.pidevice.WGO( self.wave_generator, mode=0 ) # mode = 0 stop the wavegeneration logger.info( """Stopped wave generators {} for {}.""".format( self.wave_generator, self.name ) ) self.position = self.pidevice.qPOS(axes=self.axis)[self.axis] self.__relative_position_in_fs() self.wavegen_running = False
#! Find out whether position can be queried during #! wavegeneration.
[docs] def setup_coherence_time_scan( self, coherence_time: float, frequency: float, speedupdown: int, buffer: float = 800, overshoot_factor: float = 0.1, ): """ Calculates essential parameters for waveform generation such as number of points and tablerate from a given frequency. Also creates corresponding wavetable on controller. Args: coherence_time (float): Maximum time in ps between the fixed pulse and the movable pulse. frequency (float): Frequency in Hz with which the stage should move back and forth. Advisable to choose in such a way that it does not have periodicity with the laser frequency. speedupdown (int): Sets how round the curve becomes at minimal/maximal amplitude. Necessary so that the motor does not make hard stops at these endpoints. buffer (float, optional): "Negative coherence time" - Timerange in fs where fixed pulse comes before movable pulse. This is required to collect the interferogram as a whole ("both sides"). Defaults to 800 in fs. overshoot_factor (float, optional): When using the wavegeneration functionality in combination with the interferometer counter it was observed that the actual amplitude (coherence time) decreases with an increase in frequency (velocity) of the wavegeneration and increases with an increase of the the speedupdown parameter. To counter this the overshoot_factor is used to increase the setup coherence time by its factor on both sides. Actual amplitude = coherence_time*(1+2*overshoot_factor) + buffer Defaults to 0.1. """ # Update or initialize attributes self.coherence_time = coherence_time self.speed_up_down = speedupdown self.overshoot_factor = overshoot_factor # Calculate coherence time in mm coherence_time_mm = self.__convert_fs_to_mm( coherence_time * 1000 ) # convert ps -> fs logger.info( "Coherence time (without overshoot) in mm is {} for stage {}.".format( coherence_time_mm, self.name ) ) # Calculate overshooting distance in mm that is going to be # added to both ends self.overshoot_mm = coherence_time_mm * overshoot_factor # Calculate buffer (negative coherence time) in mm buffer_mm = self.__convert_fs_to_mm(buffer) logger.info( "Buffer (without overshoot) in mm is {} for stage {}.".format( buffer_mm, self.name ) ) # To set the starting point (offset) we need to distinguish # between the two possible directionalities (-1 or +1) of the # stage. if self.direction == 1: # The offset (lowest position of stage) is the position of # the temporal overlap of both pulses minus the negative # coherence time minus the overshoot offset = self.t_zero - buffer_mm - self.overshoot_mm # mm # The reset position is the position at which the # interferometer counter should be reset to 0. Conceptually, # this corresponds to the minimum position of the coherence # time scan if the stage would work as intended. self.reset_position = self.t_zero - buffer_mm # mm #! --- self.direction = -1 does NOT work --- #! it is also missing buffer factor / reset position elif self.direction == -1: offset = self.t_zero - coherence_time_mm # Account for when coherence time is too big and direction # is negative if offset < self.__convert_fs_to_mm(self.max_range_in_fs): logger.warning( "Offset of wavegen is beyond lower limit of {}. Adjusting coherence time.".format( self.name ) ) # Calculate maximum possible coherence time in fs self.coherence_time = self.t_zero_in_fs - self.min_range_in_fs # Recalculate the corresponding coherence time in mm coherence_time_mm = self.__convert_fs_to_mm(self.coherence_time) # From maximum possible coherence time calculate minimum # offset offset = self.t_zero - coherence_time_mm logger.info( "Offset (including overshoot): {} mm for wavegen for {}.".format( offset, self.name ) ) # The amplitude is the coherence time plus the time difference # where the moving pulse comes after the fixed pulse (buffer). self.amplitude = coherence_time_mm + buffer_mm + 2 * self.overshoot_mm # Account for when the coherence time is too big and direction # is positive if self.amplitude + offset > self.__convert_fs_to_mm(self.max_range_in_fs): logger.warning( "Amplitude is of wavegen is beyond upper limit of {}. Adjusting coherence time.".format( self.name ) ) # Calculate maximum possible coherence time in fs self.coherence_time = self.max_range_in_fs - self.__convert_mm_to_fs(offset) # Recalculate the corresponding coherence time in mm coherence_time_mm = self.__convert_fs_to_mm(self.coherence_time) # From maximum possible coherence time calculate maximum # amplitude self.amplitude = coherence_time_mm + buffer_mm logger.info( "Amplitude (including overshoot): {} mm for wavegen for {}.".format( self.amplitude, self.name ) ) # Find wavegenparameters that approximate the given frequency number_of_points, tablerate = find_wavegen_params(frequency)[0] # Calculate the closest possible feasible frequency. See C-413 # user manual provided by PI. Chapter 8.6 Wave Generator # (p.151-176, especially p. 167) self.approx_frequency = 1 / (200e-6 * number_of_points * tablerate) logger.info( """The approximated wavegeneration frequency is {} Hz for stage {}.""".format( self.approx_frequency, self.name ) ) # Run a special case if number of point is too low if number_of_points < 2000: logger.warning( """The chosen frequency could only be approximated with a number of points < 2000 (namely {}). This could cause problems. Stage: {}""".format( number_of_points, self.name ) ) # todo automate frequency change, if no_of_points to low else: logger.info( """The chosen frequency is approximated with {} number of points and a tablerate of {}. Stage: {}""".format( number_of_points, tablerate, self.name ) ) # Initialize wavegeneration table first_point = 1 self.init_wavegen( number_of_points, self.amplitude, tablerate, offset, first_point, speedupdown, number_of_cycles=0, )
def __str__(self): information = """ Controller name: {} Controller ID: {} Stage name: {} Connected axes: {} Axes referenced: {} Servos activated : {} Maximum position: {} fs Minimum position: {} fs Position: {} in fs Direction: {} Path factor: {} Velocity: {} in mm/s Is moving: {} """.format( self.controller.controller_name, self.controller.id_controller, self.name, self.axis, self.pidevice.qFRF(), self.pidevice.qSVO(), self.max_range_in_fs, self.min_range_in_fs, self.pidevice.qPOS(axes=self.axis), self.direction, self.path_factor, self.velocity, self.pidevice.IsMoving(), ) return information def __enter__(self): return self def __exit__(self, type, value, tb): # Reset velocity to something humane self.set_velocity_mm(1) # Stop wavegen if running if hasattr(self, "wave_generator"): self.stop_wavegen() logging.info("Stopped wavegeneration for {}.".format(self.name))
if __name__ == "__main__": logging.basicConfig(level=logging.INFO) # # PiController C-413 # interferometer_ctrl_name = "interferometer controller (C-413)" # interferometer_ctrl = "C-413" # interferometer controller # interferometer_stage_names = ["V-522.1AA"] # interferometer_ctrl_port = "COM9" #----------------- # #PiStage C-413: V522.1AA # #Todo wir sollten das in der json file pump interferometer nennen (oder sowas, falls es irgendwann mal mehrere gibt) # interferometer_axis = "1" # interferometer_t_0 = 0.640068 # This is in mm! # interferometer_init_vel = 1 # mm/s # interferometer_name = "Interferometer Stage" # # Interferometer wavegen settings # ifm_nop = 4096 # interferometer_amplitude = 2 # Unit in mm # table_rate = 3 # firstpoint = 1 # offset = interferometer_t_0 # Unit in mm # speedupdown = 1 # minimum_position = interferometer_amplitude/2 # with PiController(interferometer_ctrl, interferometer_stage_names, com_port=interferometer_ctrl_port) as interferometer_controller, \ # PiStage(interferometer_controller, interferometer_axis, interferometer_t_0, interferometer_init_vel, name=interferometer_name) as interferometer_stage: # offset = interferometer_stage.t_zero - minimum_position #? Do we need to calculate plus or minus? # # Move interferometer back and forth # # Create wavetable that defines movement pattern # interferometer_stage.init_wavegen(ifm_nop, interferometer_amplitude, # table_rate, offset, # firstpoint, speedupdown, number_of_cycles=0) # # Start movement defined by wavetable # interferometer_stage.start_wavegen() # input() # PiController C-843 delay_ctrl_name = "IR stage controller (C-843)" delay_ctrl = "C-843" # delay controller delay_stage_names = ["M-531.PD1"] delay_ctrl_port = 1 # ----------------- # PiStage C-843: M-531.PD1 delay_axis = "1" delay_t_0 = 150 # This is in mm! delay_init_vel = 10 # mm/s delay_name = "IR Stage" with PiController( delay_ctrl, delay_stage_names, pci_board_number=delay_ctrl_port ) as delay_controller, PiStage( delay_controller, delay_axis, delay_t_0, delay_init_vel, name=delay_name ) as delay_stage: delay_stage.move(-1e5) delay_stage.move(0)