Source code for hardware_interfaces.triax

"""
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
[docs] def form_unload(self): """Always closes COM port and moves motors 1, 2 to position 0. """ # * VB: Private Sub Form_Unload (Cancel As Integer) # * VB: Not called explicitly within mess.frm ??? # ? 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)