# 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)