"""
Name:
triax - various functions for TRIAX spectrometer control
Description:
This module provides the class Triax to control TRIAX spectrometers
over RS232 to the extent that was used in the original mess.frm
Visual Basic Script.
This is therefore NOT (yet) a complete implementation of the entire interface
as given in the doc, since the other command were apparently not needed.
Warning:
When the spectrometer looses power unexpectedly during
operation, normal init may fail. It seems that the communications buffer
must be cleared first, and the new init procedure does just that. It has
not yet been tested though.
Classes:
Triax
Methods:
main()
For testing purposes: establish communication and read current state
Versions:
200322 - initial commit, Georg Wille
References:
Spectrometer_control_interfacing_programming_manual.pdf
"""
# Import relevant modules
import sys
import time
import serial
import numpy as np
from textwrap import dedent
import logging
logger = logging.getLogger(__name__)
# Helper modules
# import logging, logging.handlers
# # Set up logger
# if __name__ == "__main__":
# logger = logging.getLogger("TRIAX")
# else:
# logger = logging.getLogger(__name__)
# logger.setLevel(logging.INFO) # change later
# c_handler = logging.StreamHandler() # for console output
# c_handler.setFormatter(
# logging.Formatter("%(asctime)s : %(name)s : %(levelname)s : %(message)s")
# )
# # c_handler.setLevel(logging.info)
# # if level is not set explicitely, it is inherited from logger
# logger.addHandler(c_handler)
# # f_handler = logging.FileHandler("triax_file.log")
# # or rather more fancy:
# f_handler = logging.handlers.RotatingFileHandler(
# "triax_file.log", maxBytes=2 ** 20, backupCount=5
# )
# f_handler.setFormatter(
# logging.Formatter("%(asctime)s : %(name)s : %(levelname)s : %(message)s")
# )
# # f_handler.setLevel(logging.info)
# logger.addHandler(f_handler)
[docs]class Triax:
"""
Represents a Triax spectrometer connected via serial port.
By default, object instantiation only tries to set up communication
and, if successful, read out the current state. Hardware
initialization has to be requested by parameter or performed
separately afterwards.
The following attributes have a method for setting them via a
set_xxx method, all others are derived from there:
* grating
* slit
* wavelength
* wavenumber
Args:
hwconfig (hwconfig object): object provided by hardware
properties module, containing all hardware information. **IF
SPECIFIED, THIS SUPERSEDES ALL EXPLICITLY GIVEN SPECTROMETER AND
MCT PARAMETERS.**
port (str, optional): serial port name. Defaults to 'COM1'
turret (str, optional): turret name. Defaults to "default".
Purely decorative.
gamma (float, optional): angle (degrees) between normal of
center wavelength beam and focal plane. Defaults to -9.984
d (float, optional): angle (degrees) between entrance and exit
arm, fixed for given spectrometer geometry. Defaults to 30.0
f (float, optional): focal length (mm). Defaults to 190.0
w (float, optional): entrance slit width (mm) solely for
calculating the wavelength axis. Defaults to 2
step_slit (float, optional): factor for conversion of slit motor
position to true slit width. slit width = step_slit * slit
position. Defaults to 2.0
pixel_pitch (float, optional): center-to-center distance of MCT
pixels (mm). Defaults to 0.25
pixel_number (int, optional): number of pixels per line.
Defaults to 64.
pixel_center (int, optional): index of pixel that gets the
central wavelength. Defaults to 32.
tgratings (list, optional): lines/mm for gratings in positions
0,1,2 of current turret. Defaults to [300., 150., 100.]
init (bool, optional): immediate hardware init. Defaults to
False
baudrate (int, optional): baud rate. Defaults to 19200
bytesize (optional): data bits. Defaults to serial.EIGHTBITS
parity (optional): parity bit. Defaults to serial.PARITY_NONE
stopbits (optional): number of stop bits. Defaults to
serial.STOPBITS_ONE
timeout (float, optional): timeout in seconds. Defaults to 1.0
xonxoff (bool, optional): enable software flow control. Defaults
to True
Attributes:
ser: serial port connection object
is_connected (bool): spectrometer is properly communicating
grating (int): current grating (one of 0, 1, 2)
gr_line_density (float): current grating line density in
lines/mm
slit (float): current slit width in µm
slit_pos (int): current slit position in motor steps
wavelength (float): current true central wavelength in nm
wavelength_base (float): current central wavelength in nm w.r.t
to base grating of 1200 l/mm
wavenumber (float): current central wavenumber in cm-1
wn_axis (ndarray): list of wavenumbers in cm-1 on each detector
pixels, length of list depending on pixel number from hardware
setting
wl_axis (ndarray): list of wavelengths in nm on each detector
pixels, length of list depending on pixel number from hardware
setting
turret (str): current turret
tgratings (list): lines/mm for gratings in positions 0,1,2 of
current turret.
step_slit (float): factor for conversion of slit motor position
to true slit width. slit width = step_slit * slit position
gamma (float): angle (degrees) between normal of center
wavelength beam and focal plane
d (float): angle (degrees) between entrance and exit arm, fixed
for given spectrometer geometry
f (float): focal length (mm)
w (float): entrance slit width (mm) for wavelength axis
calculation
pixel_pitch (float): center-to-center distance of MCT pixels
(mm)
pixel_number (int): number of pixels per line
pixel_center (int): index of pixel that gets the central
wavelength
"""
MIN_WAVELENGTH = 2500.0 # in nm. This is 4000 cm-1
MAX_WAVELENGTH = 20000.0 # in nm. This is 500 cm-1
# -------------------------------------------------------
# Below are core methods those are needed for functionality
# -------------------------------------------------------
[docs] def __init__(
self,
# following values taken from the LabVIEW/mathscript module
hwconfig=None, # if given, this supersedes all directly specified spectrometer and mct values
port: str = "COM1",
turret: str = "default",
gamma: float = -9.984, # angle between normal of center wavelength beam and focal plane
d: float = 30.0, # angle between entrance and exit arm, fixed for given spectrometer geometry
f: float = 190.0, # focal length
w: float = 2.0, # entrance slit width for the purpose of calculating wavelength axis
step_slit: float = 2.0, # factor for conversion of slit motor position to true slit width
# slit width = step_slit * slit position according to VB script
# this number is estimated
pixel_pitch=250e-3, # in mm
pixel_number=64, # 64 in H-lab, 31 (!) in I-lab, may change to 32
pixel_center=32, # pixel of the center wavelength (32 according to Luuk)
tgratings: list = None, # mutable default values like lists must be avoided here, is set below
# the following are NOT contained in the hardware object
init=False,
baudrate: int = 19200,
bytesize=serial.EIGHTBITS,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
timeout=1.0,
xonxoff=True,
):
"""
Initializes new spectrometer object instance and establish
communication.
"""
if hwconfig is not None:
logger.info(
"Hardware property object used, therefore any directly specified parameters are ignored."
)
self.port = hwconfig.spectrometer.port
self.turret = hwconfig.spectrometer.active_turret
self.gamma = hwconfig.spectrometer.gamma
self.d = hwconfig.spectrometer.d
self.f = hwconfig.spectrometer.focal_length
self.w = hwconfig.spectrometer.w
self.step_slit = hwconfig.spectrometer.step_slit
self.pixel_pitch = hwconfig.mct_array.pixel_pitch
self.pixel_number = hwconfig.mct_array.number_of_pixels_per_row
self.pixel_center = hwconfig.mct_array.center_pixel
self.tgratings = getattr(
hwconfig.spectrometer, hwconfig.spectrometer.active_turret
)
else:
logger.warning(
"No hardware properties specified, using any directly given numbers or defaults."
)
self.port = port
self.turret = turret
self.gamma = gamma
self.d = d
self.f = f
self.w = w
self.step_slit = step_slit
self.pixel_pitch = pixel_pitch
self.pixel_number = pixel_number
self.pixel_center = pixel_center
if tgratings:
self.tgratings = tgratings
else:
self.tgratings = [300.0, 150.0, 100.0]
logger.info("Opening port.")
self.ser = serial.Serial(
self.port, baudrate, bytesize, parity, stopbits, timeout, xonxoff
)
assert self.ser.is_open
self.is_connected = False
self.grating = None
self.gr_line_density = None
self.slit = None
self.slit_pos = None
self.wavelength = None
self.wavelength_base = None
self.wavenumber = None
self.wn_axis = None
self.wl_axis = None
logger.info("Port opened.")
if init:
init_success = self.init_spectrometer()
if not init_success:
self.ser.close()
logger.info("Port closed.")
#! the following are straight from the VB script will very likely
#! change as requirements become clear
[docs] def init_spectrometer(self):
"""Initializes spectrometer."""
# * VB: Public Sub Init_Spectrometer()
# * VB: called from Form_Load
# This is a reimplementation of the init procedure in the VBA
# program. This is not quite as elaborate as in the
# spectrometer programming manual.
# TODO implement complete startup sequence from
# TODO page 9/10 in tbe programming manual
boot = False # ? whats that?
logger.info("Initializing spectrometer.")
if not self.ser.is_open:
logger.error("Port is closed.")
return False
logger.info("Clearing output buffer.")
buffer_content = ""
while True:
resp = self.ser.read()
if resp == b"":
break
try:
buffer_content += resp.decode(encoding="cp1252")
except:
buffer_content += "§decode_error§"
logger.info("Output buffer cleared. Content was: '" + buffer_content + "'")
# check communication
for _ in range(10):
self.ser.write(b" ") # 'UTIL WHERE AM I'
time.sleep(0.2)
resp = self.ser.read() # resp meaning response
if resp in [b"*", b"B", b"F"]:
self.is_connected = True
break
else:
logger.error(
"Communication with spectrometer failed in step 1, meaning: completely. Bummer."
)
return False
if resp == b"*":
time.sleep(0.2)
self.ser.write(b"\xf7") # 'UTIL STARTUP INTELLIGENT MODE'
resp = self.ser.read()
self.ser.write(b" ") # 'UTIl WHERE AM I' again?
time.sleep(0.2)
for _ in range(10):
# ? why are we reading out 10 times? And why only 1 byte
# Because that's how it was done in the VB script -GW
resp = self.ser.read()
if resp == b"B":
break
else:
logger.error(
'Communication with spectrometer failed in step 2: while waiting for "B" response to "247"+" " command sequence.'
)
return False
if resp == b"B":
boot = True
time.sleep(0.2)
self.ser.write(b"O2000\x00") # 'UTIL START MAIN PROGRAM'
time.sleep(1.0)
self.ser.write(b" ") # 'UTIL WHERE AM I' again?
time.sleep(0.2)
for _ in range(10):
resp = self.ser.read()
if resp != b"*":
break
else:
logger.error(
'Communication with spectrometer failed in step 3: while waiting for anything but "*" response to "O2000\\x00"+" " command sequence.'
)
return False
if resp != b"F":
logger.error(
'Communication with spectrometer failed in step 4: while waiting for "F" response to "O2000\\x00"+" " command sequence.'
)
return False
if boot:
self.ser.write(b"A") # 'MOTOR INIT'
for _ in range(300):
resp = self.ser.read()
if resp == b"o": #
break
else:
logger.error(
"Initialization of spectrometer failed in step 1: timeout during boot sequence."
)
return False
self.ser.write(b"i0,0,0\x0d") # 'SLIT SET POSITION'
time.sleep(0.1)
resp = self.ser.read()
if resp != b"o":
logger.error(
'Initialization of spectrometer failed in step 2: no "o" response after SLIT SET POSITION "i0,0,0\\x0d" command sequence.'
)
return False
logger.info("Initialization successful.")
self._read_grating()
self._read_slit()
self._read_wavelength()
return True
[docs] def new_init_spectrometer(self):
"""Initializes spectrometer according to manual."""
pass
[docs] def set_grating(self, new_grating: int):
"""
Changes to the given grating position.
Sets the current turret to one of its three positions. Instance
attributes grating and gr_line_density will be changed
accordingly. If an error occurs, these will be set to None.
Args:
new_grating (int): 0, 1 or 2
"""
# * VB: Private Sub Grating_Click()
# * VB: Not called explicitly within mess.frm ???
# * according to JB: only called via GUI user interaction
if new_grating not in (0, 1, 2):
logger.error("Invalid grating selected: {}".format(new_grating))
return
logger.info("Trying to set grating.")
if new_grating == self.grating:
logger.info("Grating already in intended position.")
return
if not self.is_connected:
logger.error("Spectrometer not connected.")
return
logger.info("Moving turret.")
self.grating = None
self.gr_line_density = None
self.ser.write(
bytes("Z451,0,0,0," + str(new_grating) + "\x0d", encoding="cp1252")
)
time.sleep(0.1)
resp = self.ser.read()
if resp != b"o":
logger.error("Error during MW_X_SET_INDEX_DEVICE_POS command.")
return
while True: # ? this loop can possibly run forever???
time.sleep(0.1)
resp = self.ser.read()
self.ser.write(b"l\x0d") # 'ACC BUSY CHECK'
time.sleep(0.1)
resp = self.ser.read()
if resp != b"o":
logger.error("Error during ACC BUSY CHECK.")
return
time.sleep(0.1)
resp = self.ser.read()
if resp == b"z":
break
self._read_grating()
self._read_wavelength()
logger.info(
"Grating changed. Command was {}. Current is {}".format(
new_grating, self.grating
)
)
return
[docs] def set_slit(self, new_slit: float):
"""
Changes slit width.
Sets the slit to the new width. Instance attributes slit and
slit_pos will be changed accordingly. If an error occurs, these
will be set to None.
Args:
new_slit (float): new slit width in micrometers
"""
# * VB: Private Sub move_slit_Click()
# * VB: Not called explicitly within mess.frm ???
if not (0 < new_slit <= 2000):
logger.error("Invalid slit width selected: {}".format(new_slit))
return
logger.info("Trying to set slit.")
if not self.is_connected:
logger.error("Spectrometer not connected.")
return
logger.info("Moving slit.")
new_slit_pos = new_slit / self.step_slit
self._read_slit()
change = new_slit_pos - self.slit_pos
self.ser.write(bytes("k0,0," + str(change) + "\x0d", encoding="cp1252"))
time.sleep(0.1)
resp = self.ser.read()
if resp != b"o":
logger.error("Error during SLIT MOVE RELATIVE command.")
self.slit = None
self.slit_pos = None
return
while True: # ? this loop can possibly run forever???
time.sleep(0.1)
resp = self.ser.read()
self.ser.write(b"E") # 'MOTOR BUSY CHECK'
time.sleep(0.1)
resp = self.ser.read()
if resp != b"o":
logger.error("Error during MOTOR BUSY CHECK.")
self.slit = None
self.slit_pos = None
return
time.sleep(0.1)
resp = self.ser.read()
if resp == b"z":
break
self._read_slit()
logger.info(
"Slit changed. Command was {}. Current is {}".format(new_slit, self.slit)
)
return
[docs] def set_wavelength(self, new_wavelength):
"""
Changes central wavelength.
Sets the central wavelength to the new value. Instance
attributes wavelength, wavelength_base, wavenumber, wn_axis and
wl_axis will be set accordingly. If an error occurs, these will
be set to None.
See also:
set_wavenumber
Args:
new_wavelength (float): new central wavelength in nm
"""
# * VB: Private Sub move_wavelength_Click()
# * VB: called from:
# scan_Spectrum
# scan_SwitchProbeSpec
# scan_PumpProbe3DSpecPol
# scan_PumpProbe3DSpec
# scan_VarDelay4D_new_Spec_NPMotor
# scan_VarDelay4D_new_Spec
# scan_VarDelay4D1polspec
if not self.is_connected:
logger.error("Spectrometer not connected.")
return
logger.info("Setting wavelength.")
if new_wavelength < self.MIN_WAVELENGTH:
logger.error(
"Requested wavelength {} is too low. Autoadjusted to permitted minimum of {}.".format(
new_wavelength, self.MIN_WAVELENGTH
)
)
new_wavelength = self.MIN_WAVELENGTH
if new_wavelength > self.MAX_WAVELENGTH:
logger.error(
"Requested wavelength {} is too high. Autoadjusted to permitted minimum of {}.".format(
new_wavelength, self.MAX_WAVELENGTH
)
)
new_wavelength = self.MAX_WAVELENGTH
new_wavelength_base = new_wavelength * self.gr_line_density / 1200
# todo check types, VB forces single precision floats here (32 bit)
# for now, I will just truncate zu three decimal places
nwb_string = str(round(new_wavelength_base, 3))
self.ser.write(bytes("Z61,1," + nwb_string + "\x0d", encoding="cp1252"))
time.sleep(0.1)
resp = self.ser.read()
if resp != b"o":
logger.error(
"Error during changing wavelength (in MW_X_MOVE_WORKING_ABS_POSITION command)."
)
self.wavelength = None
self.wavelength_base = None
self.wavenumber = None
self.wn_axis = None
self.wl_axis = None
return
while True: # ? this loop can possibly run forever???
time.sleep(0.1)
resp = self.ser.read()
self.ser.write(b"E") # 'MOTOR BUSY CHECK'
time.sleep(0.1)
resp = self.ser.read()
if resp != b"o":
logger.error(
"Error during changing wavelength (in MOTOR BUSY CHECK command)."
)
self.wavelength = None
self.wavelength_base = None
self.wavenumber = None
self.wn_axis = None
self.wl_axis = None
return
time.sleep(0.1)
resp = self.ser.read()
if resp == b"z":
break
self._read_wavelength()
logger.info(
"Wavelength changed. Command was {}. Current is {}".format(
new_wavelength, self.wavelength
)
)
[docs] def set_wavenumber(self, new_wavenumber):
"""
Changes wavenumber. This is a helper function that just converts
the given wavenumber into a wavelength and calls set_wavelength.
See also:
set_wavelength
Args:
new_wavenumber (float): new wavenumber in cm-1
"""
if new_wavenumber == 0:
logger.error("wavenumber=0 is not allowed. Changed to 1000.")
new_wavenumber = 1000
new_wavelength = 10 ** 7 / new_wavenumber
self.set_wavelength(new_wavelength)
def _read_grating(self) -> int:
"""Returns current grating.
This is the turret position (0, 1 or 2), it is *not* the grating
line density or grating density. Technically, this is the
response of the spectrometer to the 'Z452' command.
This function also sets the following object attributes based on
the call results:
Args:
grating (int): current grating (0, 1, 2)
gr_line_density (float): current grating line density in
lines/mm
Returns:
int: Position of the turret, one of 0, 1 or 2
"""
# * VB: Public Function read_grating() As Integer
# * VB: called from:
# Grating_Click
# Init_Spectrometer
logger.info("Reading current grating.")
if not self.is_connected:
logger.error("Spectrometer not connected.")
return
self.grating = None
self.gr_line_density = None
self.ser.write(b"Z452,0,0,0\x0d")
time.sleep(0.1)
resp = self.ser.read()
if resp != b"o":
logger.error("Error during reading grating")
return
time.sleep(0.1)
resp = self.ser.readline()
grating = int(resp.strip())
logger.info("Grating read as {}.".format(grating))
self.grating = grating
self.gr_line_density = self.tgratings[self.grating]
return self.grating
def _read_slit(self) -> float:
"""
Returns current slit width.
This returns the slit position in micrometers. It is calculated
from the response of the spectrometer to the SLIT READ POSITION
("j") command, scaled by the hardware scaling factor step_slit.
This function also sets the following object attributes based on
the call results:
slit: current slit width in µm slit_pos: current slit position
in motor steps as returned from the "j" command
Returns:
float: Current slit width in micrometers
"""
# * VB: Public Function pos_slit()
# ? Why is here no return type? There _is_ a return value.
# * VB: called from:
# move_slit_Click
# Init_Spectrometer
logger.info("Reading current slit.")
if not self.is_connected:
logger.error("Spectrometer not connected.")
return
self.slit = None
self.slit_pos = None
self.ser.write(b"j0,0\x0d")
time.sleep(0.1)
resp = self.ser.read()
if resp != b"o":
logger.error("Error during reading slit")
return
time.sleep(0.1)
resp = self.ser.readline()
self.slit_pos = int(resp.strip())
self.slit = self.slit_pos * self.step_slit
logger.info("Slit read as {}.".format(self.slit))
return self.slit
def _read_wavelength(self) -> float:
"""
Returns current wavelength.
This returns the current true wavelength, calculated from the
response of the spectrometer to the
MW_X_READ_WORKING_ABS_POSITION ("Z62") command, which gives the
wavelength *with respect to a base grating of 1200 lines/mm.*
actual wavelength =
(response w.r.t. 1200 l/mm base grating) / (actual grating line density in l/mm) * 1200 l/mm
This function also sets the following object attributes based on
the call results:
wavelength (float): current true central wavelength in nm
wavelength_base (float): current central wavelength in nm w.r.t
to base grating of 1200 l/mm
wavenumber (float): current central wavenumber in cm-1
wn_axis (ndarray): list of wavenumbers in cm-1 on each detector
pixels, length of list depending on pixel number from hardware
setting
wl_axis (ndarray): Similarly, the list of wavelengths in nm on
each detector pixel
Returns:
float: wavelength in nm
"""
# * VB: Public Function pos_wavelength() As Single
# * VB: called from:
# Grating_Click
# Init_Spectrometer
logger.info("Reading current wavelength.")
if not self.is_connected:
logger.error("Spectrometer not connected.")
return
self.wavelength = None
self.wavelength_base = None
self.wavenumber = None
self.wn_axis = None
self.wl_axis = None
self.ser.write(b"Z62,1\x0d")
time.sleep(0.1)
resp = self.ser.read()
if resp != b"o":
logger.error("Error during reading wavelength")
return
time.sleep(0.1)
resp = self.ser.readline()
self.wavelength_base = float(resp.strip())
self.wavelength = self.wavelength_base / self.gr_line_density * 1200
self.wavenumber = 10 ** 7 / self.wavelength
# Here follows the calculation of wn_axis and the derived
# wl_axis based on the mathscript that runs in the LabVIEW code
# TODO check all those constants and move them to hardware object!!!
rad = np.pi / 180 # for conversion between rad and degrees
rad2 = 1 / rad # for conversion between degrees and rad
gamma = self.gamma
F = self.f
D = self.d
W = self.w
# stretch_factor = 1.11
stretch_factor = 1
lambda_c = self.wavelength
P_w = self.pixel_pitch
P_l = self.pixel_number
P_c = self.pixel_center
# P_w = 0.0078 # ? Pixel width? unit?
# P_l = 31 # ? number of pixels? Should be 32 but one is broken?
# P_c = P_l // 2 # ? pixel in the middle of the array
L_b = F # at lambda_c; exit arm length to each wavelength located on the focal plane (mm)
L_a = F # entrance arm length (mm)
L_h = F * np.cos(
gamma * rad
) # perpendicular distance from grating or focusing mirror to the
# focal plane (mm); condition true for Czerny-Turner spectrometers
k = (
self.gr_line_density
) # !!! on the horiba web site k and n used the other way round!
n = 1 # diffraction order
CALIB_ERROR = 0 # hardware specific
p_lmax = 0 # First pixel number (should be one) ???
# TODO take out the superfluous calculations and stick them
# in a separate convenience function that just shows the
# calculated characteristics as Luuk's script does. Only the
# wn_axis and wl_axis are required for the other modules
##### STEP 1. Calculating alpha
beta_lc = (
rad2 * np.arcsin(1e-6 * k * n * lambda_c / (2 * np.cos(rad * D / 2)))
+ D / 2
)
alpha = beta_lc - D # at lambda_c
beta_h = (
beta_lc + gamma
) # at lambda_c; angle from L_h to the normal to the grating
H_blc = F * np.sin(gamma * rad)
H_bln = (
P_w * (P_l - P_c) + H_blc
) # in mm, positive for lambda > lambda_c, neg. for < lambda_c
H_blmax = P_w * (p_lmax - P_c) + H_blc
H_bl = np.array([H_bln, H_blc, H_blmax])
##### STEP 2. Calculating beta_ln
beta_ln = beta_h - np.arctan(H_bln / L_h) * rad2
beta_lmax = beta_h - np.arctan(H_blmax / L_h) * rad2
beta_l = np.array([beta_ln, beta_lc, beta_lmax])
##### STEP 3. Calculating lambda_n
lambda_n = (np.sin(alpha * rad) + np.sin(beta_ln * rad)) * 1e6 / (k * n)
lambda_max = (np.sin(alpha * rad) + np.sin(beta_lmax * rad)) * 1e6 / (k * n)
lambda_ = np.array([lambda_n, lambda_c, lambda_max])
##### STEP 4. Calculating dispersion nm/mm
dlambda_dx_lc = (
1e6 * np.cos(beta_lc * rad) / (n * F * k)
) # Linear dispersion at center pixel
dlambda_dx_ln = (
1e6 * np.cos(beta_ln * rad) / (n * F * k)
) # Linear dispersion at first pixel
dlambda_dx_lmax = (
1e6 * np.cos(beta_lmax * rad) / (n * F * k)
) # Linear dispersion at last pixel
dlambda = np.array([dlambda_dx_ln, dlambda_dx_lc, dlambda_dx_lmax])
##### STEP 4.5. Calculating corrected dispersion nm/mm
dlambda_dx_lc_cor = (
1e6 * np.cos(beta_lc * rad) / (n * L_h * k) * (np.cos(gamma * rad)) ** 2
) # Linear dispersion at center pixel
dlambda_dx_ln_cor = (
1e6 * np.cos(beta_ln * rad) / (n * L_h * k) * (np.cos(gamma * rad)) ** 2
) # Linear dispersion at first pixel
dlambda_dx_lmax_cor = (
1e6 * np.cos(beta_lmax * rad) / (n * L_h * k) * (np.cos(gamma * rad)) ** 2
) # Linear dispersion at last pixel
dlambda_cor = np.array(
[dlambda_dx_ln_cor, dlambda_dx_lc_cor, dlambda_dx_lmax_cor]
)
##### STEP 5. Calculating geometrical magnification factor
# Calculating the magnification factor, called the geometric
# horizontal magnification (eq. 2-16). W is the entrance slit
# width
W_lc = W * np.cos(alpha * rad) / np.cos(beta_lc * rad) * L_b / L_a
W_ln = W * np.cos(alpha * rad) / np.cos(beta_ln * rad) * L_b / L_a
W_lmax = W * np.cos(alpha * rad) / np.cos(beta_lmax * rad) * L_b / L_a
magn = np.array([W_ln, W_lc, W_lmax])
# W_scale=W_lmax/W_ln;
##### STEP 6. Calculating Bandpass
BP = W * dlambda * magn # eg. (2.21) Horiba tutorial. Essentially the FWHM of a
# spectral line caused by the optics, not by the spectral line/transition itself
# Generating calibrated wavenumbers for all pixels to save in
# separate file this is Luuk's routine but translated to
# zero-indexing
lambda_i_wavenumber = np.zeros(P_l)
for i in range(P_l): # until pixel 31 only as we have one pixel less
H_bli = P_w * (i - P_c) + H_blc
beta_li = beta_h - np.arctan(H_bli / L_h) * rad2
lambda_i = (np.sin(alpha * rad) + np.sin(beta_li * rad)) * 1e6 / (k * n)
lambda_i_wavenumber[i] = 1e7 / lambda_i + CALIB_ERROR
lambda_i_wavenumber = (
stretch_factor * (lambda_i_wavenumber - 1e7 / lambda_c) + 1e7 / lambda_c
)
self.wn_axis = lambda_i_wavenumber
self.wl_axis = 10 ** 7 / lambda_i_wavenumber
# TODO calculate the pixel calibration list and set attribute
logger.info("Wavelength is {}.".format(self.wavelength))
return self.wavelength
# -------------------------------------------------------
# Below are helper and convenience methods
# -------------------------------------------------------
[docs] def end(self):
"""
Puts Triax spectrometer in defined state and ends communication.
"""
pass
def __enter__(self):
return self
def __exit__(self, type, value, traceback):
self.end()
def __repr__(self):
repr = """\
ser: {}
is_connected: {}
turret: {}
turret gratings: {}
step_slit: {}
gamma: {}
d: {}
f: {}
w: {}
pixel_pitch: {}
pixel_number: {}
pixel_center: {}
grating: {}
gr_line_density: {}
slit: {}
slit_pos: {}
wavelength: {}
wavelength_base: {}
wavenumber: {}
wn_axis: {}
wl_axis: {}
""".format(
self.ser,
self.is_connected,
self.turret,
self.tgratings,
self.step_slit,
self.gamma,
self.d,
self.f,
self.w,
self.pixel_pitch,
self.pixel_number,
self.pixel_center,
self.grating,
self.gr_line_density,
self.slit,
self.slit_pos,
self.wavelength,
self.wavelength_base,
self.wavenumber,
self.wn_axis,
self.wl_axis,
)
return dedent(repr)
[docs] def cli(self, init=False):
"""
Implements a tiny command line interface for TRIAX access
Args:
init (Boolean, optional): call init procedure first?
Defaults to False.
"""
logger.info("Starting interactive session.")
def print_response():
while True:
resp = self.ser.read()
if resp == b"":
break
print(resp)
def print_help():
print("!HELP : Print this help")
print("!STOP : End command line interface")
print("!LIST : List TRIAX commands")
print("!READ : Read pending output from spectrometer")
print("\\x## : Enter unprintable byte value ## (hex)")
print(" e. g. 0=\\x00, 13=\\x0d, 222=\\xde, 247=\\xf7, 248=\\xf8")
def print_commands():
commands = """
!!! Refer to the manual for command parameters and responses !!!
UTIL WHERE AM I chr(32) \\x20 (=SPACE)
UTIL STARTUP INTELLIGENT MODE chr(247) \\xf7
UTIL SET INTELLIGENT MODE chr(248) \\xf8
UTIL SET TERMINAL MODE y
UTIL START MAIN PROGRAM O2000 +chr(0)
UTIL RE-BOOT IF HUNG chr(222) \\xde
UTIL READ MAIN VERSION z
UTIL READ BOOT VERSION y
UTIL CHANGE IEEE 488 ADDRESS E
MOTOR INIT A
MOTOR SET SPEED B
MOTOR READ SPEED C
MOTOR BUSY CHECK E
MOTOR MOVE RELATIVE F
MOTOR SET POSITION G
MOTOR READ POSITION H
MOTOR LIMIT STATUS K
MOTOR STOP L
SLIT SET SPEED g
SLIT READ SPEED h
SLIT SET POSITION i
SLIT READ POSITION j
SLIT MOVE RELATIVE k
ACC SHUTTER OPEN W
ACC SHUTTER CLOSE X
ACC TURRET POSITION 1 a
ACC TURRET POSITION 0/ b
ACC ENTR MIRROR SIDE c
ACC ENTR MIRROR FRONT d
ACC EXIT MIRROR SIDE e
ACC EXIT MIRROR FRONT f
ACC BUSY CHECK l
ACQ MEASURE OFFSETS w
ACQ SET OFFSETS x
ACQ CHANNEL GAIN SET R
ACQ GAIN READ S
ACQ INTEGRATION TIME SET O
ACQ INTEGRATION TIME READ P
MW_X_INDEX_DEVICE_STATUS Z453
MW_X_READ_INDEX_DEVICE_POS Z452
MW_X_SET_INDEX_DEVICE_POS Z451
MW_X_READ_WORKING_ABS_POSITION Z62
MW_X_MOVE_WORKING_ABS_POSITION Z61
MW_X_SET_WORKING_ABS_POSITION Z60
SCAN GET DATA u
SCAN GET DATA POINT NUMBER t
SCAN CYCLE TO READ s
SCAN CURRENT STATUS r
SCAN STOP v
SCAN START q
THRESHOLD READ VALUES J
THRESHOLD WRITE VALUES I
TTL READ INPUT o
TTL WRITE OUTPUT m
HIGH VOLTAGE READ V
HIGH VOLTAGE SET U
ACQ BUSY Q
ACQ STOP N
ACQ START M
"""
print(dedent(commands))
if init:
print("Initializing spectrometer.")
logger.info("Initializing spectrometer.")
self.init_spectrometer()
print_help()
print("Checking pending output from spectrometer")
print_response()
cmd = ""
while True:
cmd = input("Command: ")
if cmd == "!STOP":
break
elif cmd == "!HELP":
print_help()
elif cmd == "!LIST":
print_commands()
elif cmd == "!READ":
print_response()
else:
cmd_bytes = bytes(cmd, encoding="cp1252")
self.ser.write(cmd_bytes)
print_response()
logger.info("Ending interactive session.")
return
[docs] def status(self) -> dict:
"""Returns spectrometer status as dictionary.
Returns:
dict: [description]
"""
pass
# ? Is this still useful?
# -------------------------------------------------------
# Below are obsolete methods basically, those were needed for some
# reason in VBA but are not required here
# -------------------------------------------------------
[docs] def exit_click(self):
"""If no collection is running: closes COM port and
moves motors 1 and 2 to position 0.
"""
# * VB: Private Sub Exit_Click()
# ? What are motors 1 and 2?
pass
if __name__ == "__main__":
# Add directories to path for imports
import os, sys, inspect
currentdir = os.path.dirname(
os.path.abspath(inspect.getfile(inspect.currentframe()))
)
parentdir = os.path.dirname(currentdir)
sys.path.insert(0, os.path.join(currentdir, "ui_files"))
sys.path.insert(0, parentdir)
sys.path.insert(0, os.path.join(parentdir, "hardware_interfaces"))
from hardware_properties import HardwareProperties
path = "../hardware_config_files/h-lab_hardware_properties.json"
# path = "../hardware_config_files/i-lab_hardware_properties.json"
hw = HardwareProperties(path)
my_triax = Triax(hwconfig=hw)
my_triax.init_spectrometer()
# my_triax.set_grating(0)
# my_triax.set_wavelength(4705)
# print(my_triax.status())
print(my_triax)
sys.exit(0)