diff --git a/Config/config.json b/Config/config.json new file mode 100644 index 0000000..9060893 --- /dev/null +++ b/Config/config.json @@ -0,0 +1,6 @@ +{ + "Number_of_cycles": 40, + "Amplitude_mm": 10, + "Time_in_beam_s": 5, + "Time_out_of_beam_s": 10 +} \ No newline at end of file diff --git a/Config/paths.json b/Config/paths.json new file mode 100644 index 0000000..7453ff7 --- /dev/null +++ b/Config/paths.json @@ -0,0 +1,5 @@ +{ +"meas_scripts_dir": 'C:\Users\berti_r\Python_Projects\metrology\metrology' +"meas_scripts_dir_local": r"C:\Users\berti_r\Python_Projects\StagePerformaceDocu\Scripts" +"config_path": r"C:\Users\berti_r\Python_Projects\StagePerformaceDocu\Config\config.json" +} \ No newline at end of file diff --git a/Scripts/Temperatur_logger.py b/Scripts/Temperatur_logger.py new file mode 100644 index 0000000..3d1d16e --- /dev/null +++ b/Scripts/Temperatur_logger.py @@ -0,0 +1,43 @@ +import sys +from pathlib import Path +import threading +import queue +import paho.mqtt.client as mqtt +import time + +# Global variable to store received message +received_data = None + +def __on_connect(client, userdata, flags, rc): + print("Connected with result code " + str(rc)) + client.subscribe("test/topic") # Change this to your topic + +def __on_message(client, userdata, msg): + global received_data + received_data = msg.payload.decode() + #TODO: Write data to pickle and in queue + print(f"Message received on {msg.topic}: {received_data}") + +def __mqtt_thread(): + client = mqtt.Client() + client.on_connect = __on_connect + client.on_message = __on_message + + client.connect("broker.hivemq.com", 1883, 60) # Replace with your broker address and port + client.loop_forever() + +def start_mqtt_in_thread(): + thread = threading.Thread(target=__mqtt_thread, daemon=True) + thread.start() + +# Example usage +if __name__ == "__main__": + start_mqtt_in_thread() + + # Simulate main thread work + while True: + if received_data: + print(f"Main thread sees data: {received_data}") + received_data = None # Reset after handling + time.sleep(1) + diff --git a/Scripts/__pycache__/ad.cpython-313.pyc b/Scripts/__pycache__/ad.cpython-313.pyc new file mode 100644 index 0000000..f4307e2 Binary files /dev/null and b/Scripts/__pycache__/ad.cpython-313.pyc differ diff --git a/Scripts/__pycache__/camera.cpython-313.pyc b/Scripts/__pycache__/camera.cpython-313.pyc new file mode 100644 index 0000000..2e3c5fd Binary files /dev/null and b/Scripts/__pycache__/camera.cpython-313.pyc differ diff --git a/Scripts/__pycache__/image_analysis.cpython-313.pyc b/Scripts/__pycache__/image_analysis.cpython-313.pyc new file mode 100644 index 0000000..be84963 Binary files /dev/null and b/Scripts/__pycache__/image_analysis.cpython-313.pyc differ diff --git a/Scripts/__pycache__/metrology_functions.cpython-313.pyc b/Scripts/__pycache__/metrology_functions.cpython-313.pyc new file mode 100644 index 0000000..c7aad3b Binary files /dev/null and b/Scripts/__pycache__/metrology_functions.cpython-313.pyc differ diff --git a/Scripts/__pycache__/utils.cpython-313.pyc b/Scripts/__pycache__/utils.cpython-313.pyc new file mode 100644 index 0000000..85ff62a Binary files /dev/null and b/Scripts/__pycache__/utils.cpython-313.pyc differ diff --git a/Scripts/ad.py b/Scripts/ad.py new file mode 100644 index 0000000..5b05f27 --- /dev/null +++ b/Scripts/ad.py @@ -0,0 +1,202 @@ +# -*- coding: utf-8 -*- +""" +Camera module for the PCO cameras. +""" + +import numpy as np +import time + +from epics import PV + +# from tomoalign.camera import camera +import camera + +class AD(camera.Camera): + """ + Camera class for the GreyPoint camera. + """ + + # Direction of increasing row pixel index with respect to lab X-axis. + pixel_x_dir = -1 + # Direction of increasing column pixel index with respect to lab Y-axis. + pixel_y_dir = -1 + + def __init__(self, prefix="X02DA-PG-USB:cam1:"): + """ + Initialize the camera class + + Parameters + ---------- + prefix : str, optional + The PV prefix for the GP camera. + (default = "X02DA-PG-USB:cam1:") + + """ + + self.prefix = prefix + + self.pv_cam_run = PV('{}Acquire'.format(self.prefix)) + self.pv_cam_exposure = PV('{}AcquireTime'.format(self.prefix)) + self.pv_im_array = PV('{}ArrayData'.format("X02DA-PG-USB:image1:")) + self.pv_im_height = PV('{}ArraySize1_RBV'.format("X02DA-PG-USB:image1:")) + self.pv_im_width = PV('{}ArraySize0_RBV'.format("X02DA-PG-USB:image1:")) + + def start(self, wait=True): + """ + Start the camera acquisition process. + + Parameters + ---------- + wait : bool, optional + Whether to wait for the camera to acquire images and the minimum + wait time. (default=True) + + """ + + if not self.is_running(): + _start_time = time.time() + # self.pv_cam_run.put(1, wait=True) + self.pv_cam_run.put(1, wait=False) + if wait: + while not self.is_running(): + if(time.time() - _start_time) > 20: + raise Warning("PCO camera did not start " + \ + "acquiring images for the last 20 seconds. " + \ + "Giving up...") + time.sleep(0.1) + wt = self.get_wait_time() + et = time.time() - _start_time + if et < wt: + time.sleep(wt - et) + + def stop(self, wait=True): + """ + Stop the camera acquisition process. + + Parameters + ---------- + wait : bool, optional + Whether to wait for the camera to stop acquiring images and the + minimum wait time. (default=True) + + """ + + if self.is_running(): + _start_time = time.time() + # self.pv_cam_run.put(0, wait=True) + self.pv_cam_run.put(0, wait=False) + if wait: + while self.is_running(): + if(time.time() - _start_time) > 20: + raise Warning("PCO camera did not stop " + \ + "acquiring images for the last 20 seconds. " + \ + "Giving up...") + time.sleep(0.1) + wt = self.get_wait_time() + et = time.time() - _start_time + if et < wt: + time.sleep(wt - et) + + def get_exposure_time(self): + """ + Return the camera exposure time. + """ + + return self.pv_cam_exposure.get() + + def get_image(self, wait_for_new_frame=True): + """ + Run the camera and return an image data array. + + Caution + ------- + The timing of the image data collection is asynchronous! + I.e., the timing is not deterministically coupled to the call of + :meth:`get_image`! + We simply monitor here the EPICS waveform with the image data and will + wait to receive the next image that is published to the PV from the + continuously acquiring camera. + + Parameters + ---------- + wait_for_new_frame : bool, optional + Wait for a new image data array to arrive after the call to + :meth:`get_image` has been issued. This ensures that the data + retrieved from the EPICS waveform with the image array data is not + outdated. (default = True) + + Returns + ------- + im : array-like + The image data + + """ + + self.start() + if wait_for_new_frame: + # check the timestamp of the image EPICS PV and wait for an update + # automatic auto-monitoring of the PV is disabled due to the large + # array size --> need to update the timestamp manually with + # get_timevars(). + + self.pv_im_array.get_timevars() + ts = self.pv_im_array.timestamp + changed = False + while not changed: + while not self.pv_im_array.timestamp > ts: + # wait for an update of the PV + time.sleep(0.05) + self.pv_im_array.get_timevars() + ts = self.pv_im_array.timestamp + changed = True + return self.get_image_data() + + def get_image_data(self): + """ + Return the image data array from the camera. + + Note that for the PCO cameras, the camera itself must be aquiring + images for this function to be able to grab a current frame from the + preview stream. + + Caution + ------- + The method will return the currently available array data from the + EPICS waveform record. If the camera is not running presently, this + data could potentially be very outdated! + The data is returned immediately, the method will not wait for new + data to arrive. + + Returns + ------- + im : array-like + The image data + + See also + -------- + get_image : start camera if it is not running and grab the image data + + """ + + image_size = [int(self.pv_im_height.get()), + int(self.pv_im_width.get())] + image = self.pv_im_array.get().astype('uint16') + image = np.asarray(image.reshape(image_size)) + return image + + def is_running(self): + """ + Check if the camera is running. + """ + + return bool(self.pv_cam_run.get(as_string=False)) + + + def get_wait_time(self): + """ + Return the minimum wait time in seconds required after any + configuration change (motor motion, camera start, etc.) to ensure a + valid image is returned. + """ + + return (self.get_exposure_time() / 1000.0) + 0.1 \ No newline at end of file diff --git a/Scripts/camera.py b/Scripts/camera.py new file mode 100644 index 0000000..4aceedb --- /dev/null +++ b/Scripts/camera.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +""" +Generic camera class for alignment purposes. + +Note +---- +This class should eventually be merged with a corresponding class used for data +acquisition (tomodaq package). For now, implement the bare necessities to get +started on the alignment procedures. + +""" + +class Camera: + """ + Generic camera class. + + All hardware-specific implementations of camera classes should be + subclassed from this generic class. + + """ + + # Direction of increasing row pixel index with respect to lab X-axis. + pixel_x_dir = 1 + # Direction of increasing column pixel index with respect to lab Y-axis. + pixel_y_dir = -1 + + def __init__(self): + """ + Initialize the camera class + """ + pass + + def start(self, wait=True): + """ + Start the camera acquisition process. + + Parameters + ---------- + wait : bool, optional + Whether to wait for the camera to acquire images and the minimum + wait time. (default=True) + + """ + + raise NotImplementedError() + + def get_exposure_time(self): + """ + Return the camera exposure time. + """ + + raise NotImplementedError() + + def get_image(self): + """ + Return the image array from the camera. + """ + + raise NotImplementedError() + + def get_wait_time(self): + """ + Return the minimum timeout after starting the camera to + """ + + raise NotImplementedError() + + def is_running(self): + """ + Check if the camera is running. + """ + + raise NotImplementedError() diff --git a/Scripts/image_analysis.py b/Scripts/image_analysis.py new file mode 100644 index 0000000..ed432dd --- /dev/null +++ b/Scripts/image_analysis.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +""" +Module defining various functions for basic image analysis. + +Convenience functions for directly grabing a new image and running the fitting +on this image are provided. + +""" + +import matplotlib.pyplot as plt +import scipy.ndimage as ndi +import skimage.filters + +def image_center_of_mass(image=None, binarize=True, + apply_threshold=True, threshold=None, median_filter=True, + plot=True, verbose=True): + """ + Calculate the center of mass (COM) of the intensity distribution in an image. + + Parameters + ---------- + image : 2D array-like or None, optional + The image data for which the COM is to be calculated. + binarize : bool, optional + If set to True, the COM is calculated on the binarized image. The + binarization is calculated with the given `threshold`. (default=False) + apply_threshold : bool, optional + If set to True, the image is masked with a thresholded image first to + remove a constant background before calculating the COM. Setting has no + effect if `binarize` is True. (default = True) + threshold : float or None, optional + If a value is given, it will be used as the threshold value. If set to + None, the threshold is determined automatically based on the Li + method. (default = None) + median_filter : bool, optional + Apply a median filter before calculating the COM. This helps if the + images are noisy. (default = True) + plot : bool, optional + If True, the function will attempt to plot the result (depends on + whether matplotlib is available) (default = True) + verbose : bool, optional + If True, output some information along the way. (default = True) + + """ + + if image is None: + print('WARNING: An image is needed!') + return + + if median_filter: + image = skimage.filters.median(image) + + if binarize or apply_threshold: + if threshold is None: + thrsh = skimage.filters.threshold_li(image) + else: + thrsh = threshold + im_mask = image > thrsh + if verbose: + print(f"Image thresholded at {thrsh:.1f}") + if binarize: + image = im_mask + elif apply_threshold: + image = image * im_mask + + com_y, com_x = ndi.center_of_mass(image) + + if verbose: + print(f"Center of mass (h, v): {com_x:.2f}, {com_y:.2f}") + + if plot: + plt.figure() + plt.imshow(image) + plt.axis('equal') + plt.axis('tight') + x_min, x_max = plt.xlim() + y_min, y_max = plt.ylim() + plt.vlines(com_x, y_min, y_max, linewidth=1, color='r', + label=f'horizontal COM: {com_x:.2f}') + plt.hlines(com_y, x_min, x_max, linewidth=1, color='g', + label=f'vertical COM: {com_y:.2f}') + plt.legend() + plt.show() + + return com_x, com_y \ No newline at end of file diff --git a/Scripts/metrology_functions.py b/Scripts/metrology_functions.py new file mode 100644 index 0000000..b517497 --- /dev/null +++ b/Scripts/metrology_functions.py @@ -0,0 +1,232 @@ +import time +import os +from time import sleep +import matplotlib.pyplot as plt +from pathlib import Path + +import sys + + +#error chatchign and hard code catch stuff +def check_path(path_str): + try: + path = Path(path_str) + if not path.exists(): + raise FileNotFoundError(f"Path does not exist: {path_str}") + print(f"Path exists: {path_str}") + except FileNotFoundError as e: + print(f"Error: {e}") +library_path = r"C:\Users\berti_r\Python_Projects\templates\motion_libs" +check_path(library_path) +sys.path.append(library_path) + +import motionFunctionsLib as mfl +from PIL import Image +import numpy as np +from image_analysis import image_center_of_mass +from utils import get_datestr, get_timestr +import ad + +workdir = \ + os.path.expanduser(rf'C:\Users\berti_r\Python_Projects\metrology\metrology\Data{get_datestr()}_alignment_tests') +if not os.path.exists(workdir): + os.makedirs(workdir) + +# connect to PLC using NetId and PLC Port (from AIK in the TwinCat) +plc = mfl.plc('5.17.17.136.1.1', 852) +plc.connect() +axis1 = mfl.axis(plc, 1) +#insert try catch later + + +def run_repeatability_series( + motor_pv_prefix, ntries=100, distance=None, direction=1, + meas_pos=None, settling_time=0.0, save_images=True, run_analysis=True): + + #improv + + if os.getenv("EPICS_CA_ADDR_LIST") is not None: + pass + else: + os.environ["EPICS_CA_ADDR_LIST"] = "129.129.181.64" + + + camera = ad.AD() + pixel_size = 1.1 + + savedir = os.path.join(workdir, + f'{get_timestr()}_repeatibility_{motor_pv_prefix}') + savefile = os.path.join(savedir, + f'repeatibility_{motor_pv_prefix}.dat') + os.makedirs(savedir) + + camera.start() + + #enable axis clean up later + axis1.setAcceleration(10000.0) + axis1.setDeceleration(20000.0) + axis1.setVelocity(-3) + axis1.disableAxis() + sleep(1) + axis1.enableAxis() + sleep(1) + + + for i in range(ntries): +#---------------------------------------------move------------------------------------------ + axis1.moveRelativeAndWait(-1) + sleep(0.1) + axis1.moveRelativeAndWait(1) + sleep(1) + # mot.move(meas_pos - np.sign(direction) * distance, wait=True) + # time.sleep(0.1) + # start_pos_rbv = mot.get_position(readback=True) + start_pos_rbv = 4 + # mot.move(meas_pos, wait=True) + # time.sleep(settling_time) + # meas_pos_rbv = mot.get_position(readback=True) + meas_pos_rbv = 5 +#---------------------------------------------capture------------------------------------------ + im = camera.get_image() + com_x, com_y = image_center_of_mass(im, plot=False, verbose=False) + data_str = " {:6d} {:18f} {:18f} {:8.3f} {:8.3f} {:14.3f}\n".format( + i, start_pos_rbv, meas_pos_rbv, com_x, com_y, time.time()) + # data_str = " {:6d} {:8.3f} {:8.3f} {:14.3f}\n".format( + # i, com_x, com_y, time.time()) + print(data_str, end='') + with open(savefile, 'a') as fh: + fh.write(data_str) + + if save_images: + imobj = Image.fromarray(im) + imfile = os.path.join(savedir, + f'im_{i:05d}.tif') + imobj.save(imfile) + + + if run_analysis: + print("") + analyze_repeatability(savefile, pixel_size=pixel_size, units='um') + + axis1.disableAxis() + +def analyze_repeatability(input_file, pixel_size, units='um'): + """ + Analyze and plot the result of a repeatability scan. + + Parameters + ---------- + input_file : str + The data file containing the scan data + pixel_size : float + The effective image pixel size in the same units as the motor positions + units : str, optional + The string to use for the units. (default = 'um') + + Note + ---- + The positive motor directions and the positive pixel directions used in the + analysis may not be the same! E.g., in the Y-direction, pixels are counted + from the top towards the bottom, while positive motion direction is towards + the top. + + """ + + index, start_pos, meas_pos, com_x, com_y, ts = np.loadtxt( + input_file, unpack=True) + + com_x = com_x * pixel_size + com_y = com_y * pixel_size + + pos_dir = start_pos < meas_pos + neg_dir = start_pos > meas_pos + bidir = np.any(pos_dir) and np.any(neg_dir) + + t = ts - ts[0] + + def calc_repeatability(com_vals): + + mean_val = np.mean(com_vals) + p2v_val = com_vals.max() - com_vals.min() + sigma_val = np.std(com_vals) + rms_val = np.sqrt(np.mean(np.square(com_vals - mean_val))) + + return (mean_val, p2v_val, sigma_val, rms_val) + + mean_x, p2v_x, sigma_x, rms_x = calc_repeatability(com_x) + mean_y, p2v_y, sigma_y, rms_y = calc_repeatability(com_y) + + if bidir: + mean_x_pos, p2v_x_pos, sigma_x_pos, rms_x_pos = calc_repeatability( + com_x[pos_dir]) + mean_x_neg, p2v_x_neg, sigma_x_neg, rms_x_neg = calc_repeatability( + com_x[neg_dir]) + mean_y_pos, p2v_y_pos, sigma_y_pos, rms_y_pos = calc_repeatability( + com_y[pos_dir]) + mean_y_neg, p2v_y_neg, sigma_y_neg, rms_y_neg = calc_repeatability( + com_y[neg_dir]) + + result_str = "Repeatability results\n" + + result_str += "---------------------\n" + result_str += ("{:10s} {:>12s} {:>12s} {:>12s} {:>12s}\n".format( + 'Direction', 'Peak2valley', 'RMS', '1-sima', '3-sigma' + )) + result_str += ("{:10s} {:12f} {:12f} {:12f} {:12f}\n".format( + 'X', p2v_x, rms_x, sigma_x, 3*sigma_x)) + if bidir: + result_str += ("{:10s} {:12f} {:12f} {:12f} {:12f}\n".format( + 'X-pos', p2v_x_pos, rms_x_pos, sigma_x_pos, 3*sigma_x_pos)) + result_str += ("{:10s} {:12f} {:12f} {:12f} {:12f}\n".format( + 'X-neg', p2v_x_neg, rms_x_neg, sigma_x_neg, 3*sigma_x_neg)) + result_str += ("\n") + result_str += ("{:10s} {:12f} {:12f} {:12f} {:12f}\n".format( + 'Y', p2v_y, rms_y, sigma_y, 3*sigma_y)) + if bidir: + result_str += ("{:10s} {:12f} {:12f} {:12f} {:12f}\n".format( + 'Y-pos', p2v_y_pos, rms_y_pos, sigma_y_pos, 3*sigma_y_pos)) + result_str += ("{:10s} {:12f} {:12f} {:12f} {:12f}\n".format( + 'Y-neg', p2v_y_neg, rms_y_neg, sigma_y_neg, 3*sigma_y_neg)) + + print(result_str) + result_file = os.path.splitext(input_file)[0] + "_results.dat" + with open(result_file, 'w') as fh: + fh.write(result_str) + print(f"Results saved in: {result_file:s}") + + def plot_repeatability( + ax, index, com, mean, p2v, rms, pos_mask, neg_mask): + """ + Plot the repeatability graph + """ + ax.plot(t, (com - mean), ':k', linewidth=1) + ax.plot(t[pos_mask], (com - mean)[pos_mask], 'or', + label='meas. pos. (+)') + ax.plot(t[neg_mask], (com - mean)[neg_mask], 'ob', + label='meas. pos. (-)') + ax.hlines([com.max()-mean, com.min()-mean], t.min(), t.max(), + linestyle='-.', color='k', + label=f'p-2-v: {p2v:.3f} {units:s}', linewidth=1) + ax.hlines([rms/2.0, -rms/2.0], t.min(), t.max(), + linestyle=':', color='k', + label=f'RMS: {rms:.3f} {units:s}', linewidth=1) + ax.set_xlabel("Measurement time [s]") + ax.set_ylabel(f"Pos. fluctuations [{units:s}]") + ax.legend() + + fig, (ax1, ax2) = plt.subplots(2,1,figsize=[6,8]) + plot_repeatability( + ax1, index, com_x, mean_x, p2v_x, rms_x, pos_dir, neg_dir) + plot_repeatability( + ax2, index, com_y, mean_y, p2v_y, rms_y, pos_dir, neg_dir) + ax1.set_title("Repeatability in X-direction") + ax2.set_title("Repeatability in Y-direction") + fig.tight_layout() + plt.show() + plot_file = os.path.splitext(input_file)[0] + ".pdf" + plt.savefig(plot_file) + print(f"Plot saved in {plot_file:s}") + + +#run_repeatability_series(0,1) + diff --git a/Scripts/utils.py b/Scripts/utils.py new file mode 100644 index 0000000..a483e3f --- /dev/null +++ b/Scripts/utils.py @@ -0,0 +1,328 @@ +# -*- coding: utf-8 -*- +""" +Module defining utility functions for the alignment package. +""" + +import datetime +import os +import sys + +from epics import caget, Motor + +sys.path.insert(0, '/sls/X02DA/applications/tomodaq') +""" +from tomodaq.positioner.rotations.aerotechA3200 import AerotechA3200 +from tomodaq.positioner.rotations.aerotechAutomation1 import AerotechAutomation1""" + + +def get_timestr(): + """ + Return the current timestamp as a string. + """ + + return datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + +def get_datestr(): + """ + Return the current date as a string. + """ + + return datetime.datetime.now().strftime("%Y%m%d") + +def get_beamline_config(): + """ + Return a dictionary with the current beamline configuration. + + Returns + ------- + beamline_config : dict + The current beamline configuration in a dictionary. + + """ + + es = caget('X02DA-ES1-CAM1:ENDST_SEL', as_string=True) + cam_server = caget('X02DA-ES1-CAM1:SERV_SEL', as_string=True) + camera = caget('X02DA-ES1-CAM1:CAM_SEL', as_string=True) + microscope = caget('X02DA-ES1-MS:MS_SEL', as_string=False) + microscope_name = caget('X02DA-ES1-MS:MS_SEL', as_string=True) + magnification = caget('X02DA-ES1-MS:MAGNF') + pixel_size = caget('X02DA-ES1-CAM1:ACT_PIXL_SIZE') + scintillator = caget('X02DA-ES1-MS1:SCINTIL', as_string=True) + beam_mode = caget('X02DA-OP:BEAM_MODE', as_string=True) + energy = caget('X02DA-OP-ENE:ACTUAL') + + conf = { + 'end_station': es, + 'camera_server': cam_server, + 'camera': camera, + 'microscope': microscope, + 'microscope_name': microscope_name, + 'magnification': magnification, + 'pixel_size': pixel_size, + 'scintillator': scintillator, + 'beam_mode': beam_mode, + 'beam_energy': energy, + } + + return conf + + +def get_camera(bl_config): + """ + Returns an instance of the correct camera class according to the + current beamline configuration. + + """ + + if bl_config['camera'].startswith("PCO."): + from tomoalign.camera import pco + if bl_config['camera_server'] == "Server 1": + cam_prefix = "X02DA-CCDCAM1:" + elif bl_config['camera_server'] == "Server 2": + cam_prefix = "X02DA-CCDCAM2:" + else: + raise ValueError("Unknown camera server: " \ + "{}".format(bl_config['camera_server'])) + camera = pco.PCO(prefix=cam_prefix) + elif bl_config['camera'] == "Gigafrost 1": + from tomoalign.camera import gigafrost + camera = gigafrost.Gigafrost(layout_name='GF1') + elif bl_config['camera'] == "Gigafrost 2": + from tomoalign.camera import gigafrost + camera = gigafrost.Gigafrost(layout_name='GF2') + else: + raise NotImplementedError("Camera not implemented yet: " \ + "{}".format(bl_config['camera'])) + + return camera + + +def get_microscope(bl_config): + """ + Returns an instance of the correct microscope class according to the + current beamline configuration. + + """ + + if bl_config['end_station'] == "ES1": + es_prefix = "ES1" + elif bl_config['end_station'] == "ES2": + es_prefix = "ES2" + else: + raise ValueError("Unknown endstation: {}".format( + bl_config['end_station'])) + + if bl_config['microscope'] == 0: + # This is the standard microscope + from tomoalign.microscope import microscope1 + mic_prefix = "X02DA-{}-MS1:".format(es_prefix) + microscope = microscope1.Microscope1(prefix=mic_prefix) + elif bl_config['microscope'] == 1: + # This is the 1:1 WB microscope + from tomoalign.microscope import microscope2 + mic_prefix = "X02DA-{}-MS2:".format(es_prefix) + microscope = microscope2.Microscope2(prefix=mic_prefix) + elif bl_config['microscope'] == 3: + # This is the 10x/20x WB microscope + from tomoalign.microscope import microscope4 + mic_prefix = "X02DA-{}-MS4:".format(es_prefix) + microscope = microscope4.Microscope4(prefix=mic_prefix) + elif bl_config['microscope'] == 5: + # This is the 4x WB macroscope + from tomoalign.microscope import microscope5 + # Currently, the macroscope is only supported on ES1 + #mic_prefix = "X02DA-{}-MS5:".format(es_prefix) + mic_prefix = "X02DA-ES1-MS5:" + microscope = microscope5.Microscope5(prefix=mic_prefix) + elif bl_config['microscope'] == 6: + # This is the dual head WB microscope + from tomoalign.microscope import microscope6 + # Currently, the macroscope is only supported on ES1 + #mic_prefix = "X02DA-{}-MS6:".format(es_prefix) + mic_prefix = "X02DA-ES1-MS6:" + microscope = microscope6.Microscope6(prefix=mic_prefix) + elif bl_config['microscope'] == 7: + # This is the high-NA 10x WB microscope + from tomoalign.microscope import microscope7 + # Currently, the macroscope is only supported on ES1 + #mic_prefix = "X02DA-{}-MS7:".format(es_prefix) + mic_prefix = "X02DA-ES1-MS7:" + microscope = microscope7.Microscope7(prefix=mic_prefix) + else: + raise NotImplementedError("Microscope not implemented yet: " \ + "{}".format(bl_config['microscope'])) + + return microscope + + +def get_sample_trx(bl_config): + """ + Returns an instance of the correct epics Motor for the sample TRX + translation according to the current beamline configuration. + + """ + + if bl_config['end_station'] == "ES1": + es_prefix = "ES1" + elif bl_config['end_station'] == "ES2": + es_prefix = "ES2" + else: + raise ValueError("Unknown endstation: {}".format( + bl_config['end_station'])) + + sample_trx = Motor('X02DA-{}-SMP1:TRX'.format(es_prefix)) + + return sample_trx + + +def get_sample_try(bl_config): + """ + Returns an instance of the correct epics Motor for the sample TRY + translation according to the current beamline configuration. + + """ + + if bl_config['end_station'] == "ES1": + es_prefix = "ES1" + elif bl_config['end_station'] == "ES2": + es_prefix = "ES2" + else: + raise ValueError("Unknown endstation: {}".format( + bl_config['end_station'])) + + sample_try = Motor('X02DA-{}-SMP1:TRY'.format(es_prefix)) + + return sample_try + + +def get_sample_trz(bl_config): + """ + Returns an instance of the correct epics Motor for the sample TRZ + translation according to the current beamline configuration. + + Note + ---- + End station 1 does not have a TRZ stage. A warning is printed and None is + returned as the motor. + + """ + + if bl_config['end_station'] == "ES1": + # ES1 does not have a TRZ stage + print("WARNING: End station 1 does not have a TRZ sample motion!") + sample_trz = None + elif bl_config['end_station'] == "ES2": + sample_trz = Motor('X02DA-ES2-SMP1:TRZ') + else: + raise ValueError("Unknown endstation: {}".format( + bl_config['end_station'])) + + return sample_trz + + +def get_sample_trxx(bl_config): + """ + Returns an instance of the correct epics Motor for the sample TRXX + translation according to the current beamline configuration. + + """ + + if bl_config['end_station'] == "ES1": + es_prefix = "ES1" + elif bl_config['end_station'] == "ES2": + es_prefix = "ES2" + else: + raise ValueError("Unknown endstation: {}".format( + bl_config['end_station'])) + + sample_trxx = Motor('X02DA-{}-SMP1:TRXX'.format(es_prefix)) + + return sample_trxx + + +def get_sample_trzz(bl_config): + """ + Returns an instance of the correct epics Motor for the sample TRZZ + translation according to the current beamline configuration. + + """ + + if bl_config['end_station'] == "ES1": + es_prefix = "ES1" + elif bl_config['end_station'] == "ES2": + es_prefix = "ES2" + else: + raise ValueError("Unknown endstation: {}".format( + bl_config['end_station'])) + + sample_trzz = Motor('X02DA-{}-SMP1:TRZZ'.format(es_prefix)) + + return sample_trzz + + +def get_sample_translation_all(bl_config): + """ + Returns instances of the correct epics Motors for all 5 sample + traslations TRX, TRY, TRZ, TRXX, TRZZ according to the current + beamline configuration. + + """ + + sample_trx = get_sample_trx(bl_config) + sample_try = get_sample_try(bl_config) + sample_trz = get_sample_trz(bl_config) + sample_trxx = get_sample_trxx(bl_config) + sample_trzz = get_sample_trzz(bl_config) + + return sample_trx, sample_try, sample_trz, sample_trxx, sample_trzz + + +def get_sample_roty(bl_config): + """ + This is not implemented yet as ROTY is not a normal motor record. + + This should eventually return the correct rotation positioner instance + (--> tomodaq) + + For the moment, the ROTY stages are handled separately. + + """ + + if bl_config['end_station'] == "ES1": + mot_roty = AerotechAutomation1() + elif bl_config['end_station'] == "ES2": + mot_roty = AerotechA3200() + else: + raise ValueError("Unknown endstation: {}".format( + bl_config['end_station'])) + + return mot_roty + + +def get_writer(bl_config): + """ + Returns an writer instance according to the current beamline configuration. + + """ + + if bl_config['camera'].startswith("PCO."): + userID=os.geteuid() + from pco_rclient import PcoWriter + cam_config = "/sls/X02DA/applications/tomcat-operation-scripts/daq/config_files/pco_writer.json" + if bl_config['camera_server'] == "Server 1": + cam_name = "pco1" + address="tcp://10.10.1.26:8080" + elif bl_config['camera_server'] == "Server 2": + cam_name = "pco2" + address="tcp://10.10.1.202:8080" + else: + raise ValueError("Unknown camera server: " \ + "{}".format(bl_config['camera_server'])) + + writer=PcoWriter(connection_address=address, user_id=int(userID), + cam=cam_name, config_file=cam_config) + else: + raise NotImplementedError("The camera {} does not need a writer " \ + "instance".format(bl_config['camera'])) + + return writer \ No newline at end of file diff --git a/sample.ipynb b/notebooks/sample.ipynb similarity index 85% rename from sample.ipynb rename to notebooks/sample.ipynb index 3f39699..552c14b 100644 --- a/sample.ipynb +++ b/notebooks/sample.ipynb @@ -1,188 +1,11 @@ { "cells": [ - { - "metadata": {}, - "cell_type": "markdown", - "source": [ - "g# This is a sample Jupyter Notebook\n", - "\n", - "Below is an example of a code cell. \n", - "Put your cursor into the cell and press Shift+Enter to execute it and select the next one, or click 'Run Cell' button.\n", - "\n", - "Press Double Shift to search everywhere for classes, files, tool windows, actions, and settings.\n", - "\n", - "To learn more about Jupyter Notebooks in PyCharm, see [help](https://www.jetbrains.com/help/pycharm/ipython-notebook-support.html).\n", - "For an overview of PyCharm, go to Help -> Learn IDE features or refer to [our documentation](https://www.jetbrains.com/help/pycharm/getting-started.html)." - ], - "id": "8a77807f92f26ee" - }, - { - "metadata": { - "ExecuteTime": { - "end_time": "2025-07-10T13:20:59.460075Z", - "start_time": "2025-07-10T13:20:59.332688Z" - } - }, - "cell_type": "code", - "source": [ - "import sys\n", - "\n", - "# Imports\n", - "\n", - "from IPython.display import display, clear_output\n", - "import pandas as pd\n", - "import matplotlib.pyplot as plt\n", - "import seaborn as sns\n", - "import time\n", - "import threading\n", - "import os\n", - "import glob\n", - "import numpy as np\n", - "from datetime import datetime, timedelta\n", - "\n", - "import ipywidgets as widgets\n", - "from IPython.display import display\n", - "from webcolors import names\n", - "from pathlib import Path\n", - "\n", - "def check_path(path_str):\n", - " try:\n", - " path = Path(path_str)\n", - " if not path.exists():\n", - " raise FileNotFoundError(f\"Path does not exist: {path_str}\")\n", - " print(f\"Path exists: {path_str}\")\n", - " except FileNotFoundError as e:\n", - " print(f\"Error: {e}\")\n", - "\n", - "meas_scripts_dir = r\"C:\\Users\\berti_r\\Python_Projects\\metrology\\metrology\"\n", - "check_path(meas_scripts_dir)\n", - "sys.path.append(meas_scripts_dir)\n", - "\n", - "#local includes\n", - "import metrology_functions as mf\n", - "\n", - "\n", - "# --------------------------------------------------GUI Objects-----------------------------------------------\n", - "start_button = widgets.Button(description=\"Start Measurement\")\n", - "\n", - "nr_of_cycles = widgets.BoundedIntText(\n", - " value=1,\n", - " min=1,\n", - " max=1000,\n", - " step=1,\n", - " description='Nr of cycles:',\n", - " disabled=False\n", - ")\n", - "# --------------------------------------------------Output chanels-----------------------------------------------\n", - "output1 = widgets.Output()\n", - "output2 = widgets.Output()\n", - "\n", - "# --------------------------------------------------IRQs-----------------------------------------------\n", - "def start_measurement(b):\n", - " with output1:\n", - " clear_output()\n", - " print(f\"Measurement started with {global_nr_of_cycles} cycles\")\n", - " mf.run_repeatability_series(0,global_nr_of_cycles)\n", - "\n", - "# Callback function to update the global variable\n", - "def set_nr_of_cycles(change):\n", - " global global_nr_of_cycles\n", - " global_nr_of_cycles = change['new']\n", - " with output2:\n", - " clear_output()\n", - " print(f'Nr of cycles: {global_nr_of_cycles}')\n", - "\n", - "\n", - "# --------------------------------------------------Unmask IRQs-----------------------------------------------\n", - "nr_of_cycles.observe(set_nr_of_cycles, names='value')\n", - "start_button.on_click(start_measurement)\n", - "\n", - "# --------------------------------------------------Display-----------------------------------------------\n", - "\n", - "display(nr_of_cycles, output2)\n", - "display(start_button, output1)\n" - ], - "id": "fbc121e30a2defb3", - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Path exists: C:\\Users\\berti_r\\Python_Projects\\metrology\\metrology\n", - "Constructor for PLC\n", - "Connect to PLC\n", - "is_open()=True\n", - "get_local_address()=None\n", - "read_device_info()=('Plc30 App', )\n", - "GVL_APP.nAXIS_NUM=1\n", - "Constructor for axis\n" - ] - }, - { - "data": { - "text/plain": [ - "BoundedIntText(value=1, description='Nr of cycles:', max=1000, min=1)" - ], - "application/vnd.jupyter.widget-view+json": { - "version_major": 2, - "version_minor": 0, - "model_id": "2afeeb32ecc846d3aeeab5193d357ca0" - } - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "Output()" - ], - "application/vnd.jupyter.widget-view+json": { - "version_major": 2, - "version_minor": 0, - "model_id": "2291ed5151aa4d10aa4f780784e61869" - } - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "Button(description='Start Measurement', style=ButtonStyle())" - ], - "application/vnd.jupyter.widget-view+json": { - "version_major": 2, - "version_minor": 0, - "model_id": "28a931d72e9644d88ddbbd2e0c91e825" - } - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "Output()" - ], - "application/vnd.jupyter.widget-view+json": { - "version_major": 2, - "version_minor": 0, - "model_id": "68db2138d0234caf8ee3065f00de3734" - } - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "execution_count": 1 - }, { "metadata": {}, "cell_type": "markdown", "source": [ "# System description and Goals\n", - "![assambly](Images_doku/Overview.png)\n", + "![assambly](../Images_doku/Overview.png)\n", "\n", "## Performance criteria\n", "### Hard\n", @@ -206,6 +29,15 @@ "description of movment patterns and req. of expected use in beamline\n", "\n", "## Posible enviromental impacts and measurement methods\n", + "- Vibrations\n", + "- Temperatur changes\n", + "- Backlash\n", + "- physical shape of screwdrive\n", + "- physical shape of rail\n", + "- Motor break induced force\n", + "- Motor properties\n", + "- Humidity\n", + "- enc resolution\n", "\n" ], "id": "ac98fcd46a8e41f9", @@ -218,6 +50,203 @@ } } }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-07-14T15:22:58.513182Z", + "start_time": "2025-07-14T15:22:58.266333Z" + } + }, + "cell_type": "code", + "source": [ + "import sys\n", + "\n", + "# Imports\n", + "\n", + "from IPython.display import display, clear_output\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "import seaborn as sns\n", + "import time\n", + "import threading\n", + "import os\n", + "import json\n", + "import glob\n", + "import numpy as np\n", + "from datetime import datetime, timedelta\n", + "\n", + "import ipywidgets as widgets\n", + "from IPython.display import display\n", + "from pywin.debugger import close\n", + "from webcolors import names\n", + "from pathlib import Path\n", + "#TODO: Move script from frederica to scripts\n", + "def check_path(path_str):\n", + " try:\n", + " path = Path(path_str)\n", + " if not path.exists():\n", + " raise FileNotFoundError(f\"Path does not exist: {path_str}\")\n", + " print(f\"Path exists: {path_str}\")\n", + " except FileNotFoundError as e:\n", + " print(f\"Error: {e}\")\n", + "\n", + "meas_scripts_dir = r\"C:\\Users\\berti_r\\Python_Projects\\metrology\\metrology\"\n", + "meas_scripts_dir_local = r\"C:\\Users\\berti_r\\Python_Projects\\StagePerformaceDocu\\Scripts\"\n", + "config_path = r\"C:\\Users\\berti_r\\Python_Projects\\StagePerformaceDocu\\Config\\config.json\"\n", + "\n", + "#check_path(meas_scripts_dir)\n", + "check_path(meas_scripts_dir_local)\n", + "check_path(config_path)\n", + "sys.path.append(meas_scripts_dir_local)\n", + "#sys.path.append(meas_scripts_dir)\n", + "#TODO: mirror struct from jason\n", + "\n", + "#local includes\n", + "import metrology_functions as mf\n", + "\n", + "# Load config from JSON file\n", + "def load_config():\n", + " with open(config_path, 'r') as f:\n", + " return json.load(f)\n", + "\n", + "# Save updated config to JSON file\n", + "def save_config(updated_config):\n", + " with open(config_path, 'w') as f:\n", + " json.dump(updated_config, f, indent=4)\n", + "\n", + "# Get number of cycles from config\n", + "def init_nr_of_cycles():\n", + " config = load_config()\n", + " return config.get(\"Number_of_cycles\", 1)\n", + "\n", + "\n", + "\n", + "\n", + "# --------------------------------------------------GUI Objects-----------------------------------------------\n", + "start_button = widgets.Button(description=\"Start Measurement\")\n", + "\n", + "nr_of_cycles = widgets.BoundedIntText(\n", + " value=init_nr_of_cycles(),\n", + " min=1,\n", + " max=1000,\n", + " step=1,\n", + " description='Nr of cycles:',\n", + " disabled=False\n", + ")\n", + "# --------------------------------------------------Output chanels-----------------------------------------------\n", + "output1 = widgets.Output()\n", + "output2 = widgets.Output()\n", + "\n", + "# --------------------------------------------------IRQs-----------------------------------------------\n", + "def start_measurement(b):\n", + " with output1:\n", + " clear_output()\n", + " local_nr_of_cycles = init_nr_of_cycles()\n", + " print(f\"Measurement started with {local_nr_of_cycles} cycles\")\n", + " mf.run_repeatability_series(0,local_nr_of_cycles) # TODO: <----- real measurement start (SFM with semaphore and queue?)\n", + " #Todo: Start temperatur Logger\n", + "\n", + "\n", + "\n", + "# Set number of cycles and save to config\n", + "def set_nr_of_cycles(change):\n", + " new_cycles = change['new']\n", + " config = load_config()\n", + " config['Number_of_cycles'] = new_cycles\n", + " save_config(config)\n", + "\n", + " with output2:\n", + " clear_output()\n", + " print(f'Number of cycles set to: {new_cycles}')\n", + "\n", + "\n", + "# --------------------------------------------------Unmask IRQs-----------------------------------------------\n", + "nr_of_cycles.observe(set_nr_of_cycles, names='value')\n", + "start_button.on_click(start_measurement)\n", + "\n", + "# --------------------------------------------------Display-----------------------------------------------\n", + "\n", + "display(nr_of_cycles, output2)\n", + "display(start_button, output1)\n" + ], + "id": "fbc121e30a2defb3", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Path exists: C:\\Users\\berti_r\\Python_Projects\\metrology\\metrology\n", + "Path exists: C:\\Users\\berti_r\\Python_Projects\\StagePerformaceDocu\\Scripts\n", + "Path exists: C:\\Users\\berti_r\\Python_Projects\\StagePerformaceDocu\\Config\\config.json\n", + "Path exists: C:\\Users\\berti_r\\Python_Projects\\templates\\motion_libs\n", + "Constructor for PLC\n", + "Connect to PLC\n", + "is_open()=True\n", + "get_local_address()=None\n", + "read_device_info()=('Plc30 App', )\n", + "GVL_APP.nAXIS_NUM=3\n", + "Constructor for axis\n" + ] + }, + { + "data": { + "text/plain": [ + "BoundedIntText(value=3, description='Nr of cycles:', max=1000, min=1)" + ], + "application/vnd.jupyter.widget-view+json": { + "version_major": 2, + "version_minor": 0, + "model_id": "1e72fea404844af69b9884b31d9fe2d1" + } + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Output()" + ], + "application/vnd.jupyter.widget-view+json": { + "version_major": 2, + "version_minor": 0, + "model_id": "7459f5fd35314ce99fce8129bda3e8e9" + } + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Button(description='Start Measurement', style=ButtonStyle())" + ], + "application/vnd.jupyter.widget-view+json": { + "version_major": 2, + "version_minor": 0, + "model_id": "40b270d1d2814cfe8d6d876a78591c0f" + } + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Output()" + ], + "application/vnd.jupyter.widget-view+json": { + "version_major": 2, + "version_minor": 0, + "model_id": "44db6c1f5acf4cb4bf04a8d3fabb1c34" + } + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "execution_count": 1 + }, { "metadata": {}, "cell_type": "code", @@ -299,6 +328,18 @@ "id": "2d155545665095ec", "outputs": [], "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "#Data loger thread\n", + "import paho-mqtt.client as mqtt\n", + "import" + ], + "id": "9813d493bd439789", + "outputs": [], + "execution_count": null } ], "metadata": {