From 3cb66fecb2e1eff6c289ad8231b0b72097f1e2a7 Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Tue, 19 Mar 2024 11:10:59 +0100 Subject: [PATCH 01/21] Move align to tomo.align --- tomo/align.py | 821 ++++++++++++++++++++++++++++++++++++++++++++ tomo/optic/align.py | 821 +------------------------------------------- 2 files changed, 823 insertions(+), 819 deletions(-) create mode 100644 tomo/align.py diff --git a/tomo/align.py b/tomo/align.py new file mode 100644 index 00000000..d565af22 --- /dev/null +++ b/tomo/align.py @@ -0,0 +1,821 @@ +import numpy as np +import click +import gevent +from bliss.common.scans import ascan, pointscan, DEFAULT_CHAIN +from bliss.common.scans.ct import ct +try: + # BLISS >= 2.0 + bliss = 2 + from blissdata.lima.image_utils import image_from_server +except ImportError: + # BLISS <= 1.11 + bliss = 1 + from blissdata.data.lima_image import image_from_server + +from bliss.common.cleanup import ( + error_cleanup, + cleanup, + capture_exceptions, + axis as cleanup_axis, +) +from bliss.common.utils import BOLD + +from matplotlib import pyplot as plt +from bliss.scanning.scan_tools import peak +from bliss.controllers.lima.roi import Roi +from bliss.shell.standard import umv, mv, umvr, flint +from bliss.scanning.chain import ChainPreset, ChainIterationPreset + +import logging + +_logger = logging.getLogger(__name__) + + + +try: + from nabu.estimation.translation import DetectorTranslationAlongBeam + from nabu.estimation.focus import CameraFocus + from nabu.estimation.tilt import CameraTilt +except ImportError: + _logger.error("Error while importing nabu", exc_info=True) + nabu = None + + +class Align: + def __init__(self, name, config): + self.__name = name + sequence = config.get("sequence") + + self.__config = config + + self.sequence = sequence + self.paper = self.__config.get("paper") + + self.slits_vgap = self.__config.get("slits_vgap") + self.slits_hgap = self.__config.get("slits_hgap") + self.detector_vtilt = self.__config.get("detector_vtilt") + self.detector_htilt = self.__config.get("detector_htilt") + + @property + def name(self): + """ + A unique name for the Bliss object + """ + return self.__name + + @property + def config(self): + """ + The configuration for Tomo alignment + """ + return self.__config + + def __info__(self): + info_str = f"{self.name} info:\n" + if self.sequence is not None: + info_str += f" tomo = {self.sequence.name} \n" + info_str += f" detector = {self.sequence._detector.name}\n" + info_str += f" optic = {self.sequence._detectors.get_optic(self.sequence._detector).description}\n" + info_str += f" pixel size = {self._pixel_size()} \n" + info_str += f" exposure time = {self.sequence.pars.exposure_time} \n\n" + + info_str += f" focus motor = {self.sequence._detectors.get_optic(self.sequence._detector).focus_motor.name} \n" + info_str += f" rotc motor = {self.sequence._detectors.get_optic(self.sequence._detector).rotc_motor.name} \n" + info_str += f" lateral motor = {self.sequence._y_axis.name} \n\n" + else: + info_str += " tomo = No Tomo object configured!\n" + if self.paper is not None: + info_str += f" alignment paper = {self.paper.name} \n" + else: + info_str += " alignment paper = No alignment pager object configured!\n" + if self.slits_vgap is not None: + info_str += f" slits vgap = {self.slits_vgap.name} \n" + else: + info_str += " slits vgap = No slits vgap object configured!\n" + if self.slits_hgap is not None: + info_str += f" slits hgap = {self.slits_hgap.name} \n" + else: + info_str += " slits hgap = No slits hgap object configured!\n" + if self.detector_vtilt is not None: + info_str += f" detector vtilt = {self.detector_vtilt.name} \n" + else: + info_str += " detector vtilt = No detector vtilt object configured!\n" + if self.detector_htilt is not None: + info_str += f" detector htilt = {self.detector_htilt.name} \n" + else: + info_str += " detector htilt = No detector htilt object configured!\n" + return info_str + + def _pixel_size(self): + # pixel size returned by tomo is in um + pixel_size = self.sequence._detectors.get_image_pixel_size( + self.sequence._detector + ) + + # pixel size for Nabu caluculations must be in mm + pixel_size = pixel_size / 1000.0 + print(BOLD("\npixel_size in mm = %g\n" % pixel_size)) + + return pixel_size + + def _get_images_bliss_1_11(self, scan, detector): + """Compatibility with BLISS 1.11""" + from blissdata.data.events import ImageNotSaved + + scan_images = [] + lima_data = scan.get_data()[detector.image] + npoints = lima_data.last_index + + if npoints < lima_data.buffer_max_number: + try: + for i in range(npoints): + scan_images.append(lima_data.get_image(i)) + + return scan_images + except ImageNotSaved: + chain_presets = next(scan.acq_chain._presets_master_list.values()) + for preset in chain_presets: + if isinstance(preset, ImageNoSavingPreset): + scan_images = preset._get_images() + return scan_images + raise + else: + ValueError("Cannot read images! Not enough images in the Lima image buffer") + + def _get_images(self, scan, detector): + # read back the scan images aquired + if bliss == 1: + return self._get_images_bliss_1_11(scan, detector) + + if scan.scan_info["save"]: + lima_data = scan.streams[detector.image] + return lima_data[:] + elif 'flat' in scan.name or 'dark' in scan.name: + lima_data = list(image_from_server(detector.proxy, -1).array) + return lima_data + else: + chain_presets = next(scan.acq_chain._presets_master_list.values()) + for preset in chain_presets: + if isinstance(preset, ImageNoSavingPreset): + lima_data = preset._get_images() + return lima_data + + # Detector focus alignment + # + + def focus(self, save=False, plot=False): + + # prepare the focus scan parameters + scan_pars = self.sequence._detectors.get_optic( + self.sequence._detector + ).focus_scan_parameters() + + scan_range = scan_pars["focus_scan_range"] + scan_steps = scan_pars["focus_scan_steps"] + focus_type = scan_pars["focus_type"] + focus_motor = self.sequence._detectors.get_optic( + self.sequence._detector + ).focus_motor + + if ( + focus_type == "Rotation" + and self.sequence._detectors.get_optic( + self.sequence._detector + ).magnification + < 0.7 + ): + scan_range /= 2 + scan_steps *= 2 + + start = focus_motor.position - scan_range + stop = focus_motor.position + scan_range + + # pixel size in mm + pixel_size = self._pixel_size() + + if self.paper: + # move in paper + print("Move in paper") + self.paper.IN() + + # Do the focus scan + focus_scan = self.focus_scan( + focus_motor, + start, + stop, + scan_steps, + self.sequence.pars.exposure_time, + self.sequence._detector, + save=save, + ) + + if focus_scan is not None: + self.focus_calc( + focus_scan, + focus_motor, + self.sequence._detector, + pixel_size, + plot, + ) + + ct(self.sequence.pars.exposure_time) + print(f"Focus motor position = {focus_motor.position}") + + # Move out paper + if self.paper and click.confirm("Move out paper?", default=True): + self.paper.OUT() + + def focus_calc( + self, + focus_scan, + focus_motor, + detector, + pixel_size, + plot=False, + ): + if focus_scan is not None: + # read back the aquired images + foc_images = self._get_images(focus_scan, detector) + # convert to numpy array + foc_array = np.array(foc_images) + + # read back the aquired positions + foc_pos = focus_scan.get_data()[focus_motor.name] + + focus_calc = CameraFocus() + # enable calculation results plotting + if plot is True: + focus_calc.verbose = True + + focus_pos, focus_ind, tilts_vh = focus_calc.find_scintillator_tilt( + foc_array, foc_pos + ) + # print (focus_pos, focus_ind, tilts_vh) + + # tilts_corr_vh_deg = -np.rad2deg(np.arctan(tilts_vh / pixel_size)) + tilts_corr_vh_rad = - tilts_vh / pixel_size + + print(f"\nBest focus position: {focus_pos}") + print(f"Tilts vert. and hor. in rad: {tilts_corr_vh_rad}\n") + + if plot is True: + pause = plt.pause(0.1) + g = gevent.spawn(pause) + if click.confirm("Move to the best focus position?", default=True): + # Always move to the best focus from the bottom + mv(focus_motor, foc_pos[0]) + # move focus motor to maximum + mv(focus_motor, focus_pos) + + # Kill the plot window when it was requested + if plot is True: + g.kill() + # workaround for nabu plotting problem + plt.close("all") + + def focus_scan( + self, + focus_motor, + start, + stop, + steps, + expo_time, + detector, + save=False, + ): + # prepare roi counters for statistics + focus_roi = Roi(0, 0, detector.image.roi[2], detector.image.roi[3]) + + det_rois = detector.roi_counters + det_rois["focus_roi"] = focus_roi + + std = detector.roi_counters.counters.focus_roi_std + counters = (std, detector.image) + + if not save: + savingpreset = ImageNoSavingPreset(detector) + DEFAULT_CHAIN.add_preset(savingpreset) + # clean-up: move back focus motor, delete roi counters, move out paper + restore_list = (cleanup_axis.POS,) + with cleanup(*[focus_motor], restore_list=restore_list): + with capture_exceptions(raise_index=0) as capture: + with capture(): + # execute focus scan + scan = ascan( + focus_motor, + start, + stop, + steps, + expo_time, + counters, + title="focus scan", + save=save, + ) + + # get the position on the maximum of the standard deviation + pos_std_max = peak() + + # delete roi counters + del det_rois["focus_roi"] + + if not save: + DEFAULT_CHAIN.remove_preset(savingpreset) + + return scan + + # test if an error has occured + if len(capture.failed) > 0: + # delete roi counters + del det_rois["focus_roi"] + + if not save: + DEFAULT_CHAIN.remove_preset(savingpreset) + + return None + + # Lateral alignment and camera tilt + + def align(self, save=False, plot=False, plot_images=False, use_fft_polar_method=False, questions_move=True): + # move rotation axis to 0 + umv(self.sequence._rotation_axis, 0) + + # take reference image + flat_n = 1 + flat_runner = self.sequence._tomo_runners["flat"] + flat_scan = flat_runner( + self.sequence.pars.exposure_time, + flat_n, + self.sequence._detector, + projection=self.sequence._inpars.projection, + save=save, + ) + flat_images = self._get_images( + flat_scan, self.sequence._detector + ) + flat_image = flat_images[-1] + + # take dark image + dark_n = 1 + dark_runner = self.sequence._tomo_runners["dark"] + dark_scan = dark_runner( + self.sequence.pars.exposure_time, dark_n, self.sequence._detector, save=save + ) + # read back the aquired images + dark_images = self._get_images( + dark_scan, self.sequence._detector + ) + dark_image = dark_images[-1] + + # pixel size in mm + pixel_size = self._pixel_size() + # do alignment scan + al_scan = self.align_scan( + self.sequence._rotation_axis, + self.sequence.pars.exposure_time, + self.sequence._detector, + save=save, + ) + + # read back the aquired images + al_images = self._get_images(al_scan, self.sequence._detector) + + # works only with on motor today! + lateral_motor = self.sequence._y_axis + rotc = self.sequence._detectors.get_optic(self.sequence._detector).rotc_motor + + source_sample_distance = self.sequence._tomo_config.source_sample_distance + sample_detector_distance = self.sequence._detectors.get_tomo_detector( + self.sequence._detector + ).sample_detector_distance + + lat_corr, camera_tilt = self.align_calc( + al_images, + dark_image, + flat_image, + lateral_motor, + rotc, + self.sequence._detector, + pixel_size, + source_sample_distance, + sample_detector_distance, + plot, + plot_images, + use_fft_polar_method, + questions_move, + ) + + ct(self.sequence.pars.exposure_time) + print(f"Lateral alignment motor position = {lateral_motor.position}") + if rotc is not None: + print(f"Camera tilt position {rotc.name} = {rotc.position}") + + return lat_corr, camera_tilt + + def correct_movement(self, d_t0, d_t1): + if abs((d_t0 - d_t1) / d_t0) > 0.05: + flag_not_moving = False + sign = ((d_t0 < 0) == (d_t1 < 0)) + if sign: + if abs(d_t0) < abs(d_t1): + dire = -1 + else: + dire = 1 + else: + dire = 1 + + value_corrected = d_t1 - d_t0 + fraction_correct = abs(d_t0/value_corrected)*dire + new_corection = d_t1*fraction_correct + else: + fraction_correct = 1 + flag_not_moving = True + new_corection = 0 + + return new_corection, fraction_correct, flag_not_moving + + def align_iter(self, yrot_precision=0.01, rotc_precision=0.001): + lateral_motor = self.sequence._y_axis + rotc = self.sequence._detectors.get_optic(self.sequence._detector).rotc_motor + + dyrot_t0, drotc_t0 = self.align(questions_move=False) + if (abs(dyrot_t0) < yrot_precision) and (abs(dyrot_t0) < rotc_precision): + flag_pass = True + else: + flag_pass = False + + if not flag_pass: + + umvr(lateral_motor, dyrot_t0, rotc, drotc_t0) + + i = 0 + f_rotc = 1.0 + f_yrot = 1.0 + + while True: + + dyrot_t1, drotc_t1 = self.align(questions_move=False) + + if (abs(dyrot_t1) > yrot_precision) and (abs(drotc_t1) > rotc_precision): + umvr(lateral_motor, dyrot_t1, rotc, drotc_t1) + elif (abs(dyrot_t1) > yrot_precision): + umvr(lateral_motor, dyrot_t1) + elif (abs(drotc_t1) > rotc_precision): + umvr(rotc, drotc_t1) + else: + break + + dyrot_t1 = f_yrot * dyrot_t1 + drotc_t1 = f_rotc * drotc_t1 + + dyrot_t1, f_yrot, flag_not_moving_yrot = self.correct_movement(dyrot_t0, dyrot_t1) + drotc_t1, f_rotc, flag_not_moving_rotc = self.correct_movement(drotc_t0, drotc_t1) + + dyrot_t0 = dyrot_t1 + drotc_t0 = drotc_t1 + + i += 1 + + def align_calc( + self, + al_images, + dark_image, + flat_image, + lateral_motor, + rotc_motor, + detector, + pixel_size, + source_sample_distance, + sample_detector_distance, + plot=False, + plot_images=False, + use_fft_polar_method=False, + questions_move=True, + ): + radio0 = (al_images[0].astype(float) - dark_image.astype(float)) / ( + flat_image.astype(float) - dark_image.astype(float) + ) + radio180 = (al_images[1].astype(float) - dark_image.astype(float)) / ( + flat_image.astype(float) - dark_image.astype(float) + ) + + # flip the radio180 for the calculation + radio180_flip = np.fliplr(radio180.copy()) + + if plot_images: + f = flint() + p = f.get_plot( + "image", + name="dark_image", + unique_name="dark_image", + selected=True, + closeable=True, + ) + + p.set_data(dark_image) + p.yaxis_direction = "down" + p.side_histogram_displayed = False + + p = f.get_plot( + "image", + name="flat_image", + unique_name="flat_image", + selected=True, + closeable=True, + ) + + p.set_data(flat_image) + p.yaxis_direction = "down" + p.side_histogram_displayed = False + + p = f.get_plot( + "image", + name="projection_image_0", + unique_name="projection_image_0", + selected=True, + closeable=True, + ) + + p.set_data(radio0) + p.yaxis_direction = "down" + p.side_histogram_displayed = False + + p = f.get_plot( + "image", + name="projection_image_180_flip", + unique_name="projection_image_180_flip", + selected=True, + closeable=True, + ) + + p.set_data(radio180_flip) + p.yaxis_direction = "down" + p.side_histogram_displayed = False + + # plot_image(radio0) + # plot_image(radio180_flip) + + # calculate the lateral correction for the rotation axis + # and the camera tilt with line by line correlation + tilt_calc = CameraTilt() + # enable calculation results plotting + if plot is True: + tilt_calc.verbose = True + + method = "1d-correlation" + if use_fft_polar_method: + method = "fft-polar" + pixel_cor, camera_tilt = tilt_calc.compute_angle( + radio0, radio180_flip, method=method + ) + print("CameraTilt: pixel_cor = %f" % pixel_cor) + + # distance from source to sample = L1(mm) + # distance from source to detector = L2(mm) + # size of the image pixel = s(mm/pixel) + # dmot(mm) = L1/L2*s*pixel_cor + # cor_factor = ( + # source_sample_distance + # / (source_sample_distance + sample_detector_distance) + # * pixel_size + # ) + + pos_cor = pixel_size * pixel_cor + + # pixel size in mm + pixel_size = self._pixel_size() + print(BOLD(f"Lateral alignment position correction in mm: {-pos_cor:>10.5f}")) + print( + BOLD( + f"Camera tilt in deg (motor rotc: {rotc_motor.name}): {camera_tilt:35.5f}\n" + ) + ) + + if plot is True: + pause = plt.pause(0.1) + g = gevent.spawn(pause) + + # if click.confirm( + # "Apply the lateral position correction and the camera tilt?", default=False): + + if questions_move: + if click.confirm("Apply the lateral position correction?", default=False): + # apply lateral correction + umvr(lateral_motor, -pos_cor) + + if click.confirm("Apply the camera tilt?", default=False): + # apply tilt correction + try: + umvr(rotc_motor, camera_tilt) + except Exception: + pass + # Kill the plot window when it was requested + if plot is True: + # workaround for nabu plotting problem + g.kill() + plt.close("all") + + return -pos_cor, camera_tilt + + def align_scan( + self, rotation_motor, expo_time, detector, save=False + ): + if not save: + savingpreset = ImageNoSavingPreset(detector) + DEFAULT_CHAIN.add_preset(savingpreset) + # clean-up: move back rot + restore_list = (cleanup_axis.POS,) + with cleanup(rotation_motor, restore_list=restore_list): + with capture_exceptions(raise_index=0) as capture: + with capture(): + # scan rot + rot_pos_list = [0.0, 180.0] + scan = pointscan( + rotation_motor, + rot_pos_list, + expo_time, + detector.image, + title="align scan", + save=save, + ) + + if not save: + DEFAULT_CHAIN.remove_preset(savingpreset) + + return scan + + # test if an error has occured + if len(capture.failed) > 0: + if not save: + DEFAULT_CHAIN.remove_preset(savingpreset) + + return None + + # Camera stage tilt alignment + + def alignxc(self, start=100, stop=800, steps=10, save=False, plot=False): + # pixel size in mm + pixel_size = self._pixel_size() + + # slit gap to use + if pixel_size < 0.003: # 3um + slit_gap = 0.2 + else: + slit_gap = 1.0 + + alxc_scan = self.alignxc_scan( + self.sequence._detector_axis, + start, + stop, + steps, + self.sequence.pars.exposure_time, + self.sequence._detector, + self.slits_vgap, + self.slits_hgap, + slit_gap, + save=save, + ) + # read back the aquired images + alxc_images = self._get_images( + alxc_scan, self.sequence._detector + ) + # print(alxc_images) + + # read back the motor positions + alxc_pos = alxc_scan.get_data()[self.sequence._detector_axis.name] + # print(al_pos) + + # take dark image + dark_n = 1 + dark_runner = self.sequence._tomo_runners["dark"] + dark_scan = dark_runner( + self.sequence.pars.exposure_time, dark_n, self.sequence._detector, save=save + ) + # read back the aquired images + dark_images = self._get_images( + dark_scan, self.sequence._detector + ) + dark_image = dark_images[0] + + distanceMove = abs(stop - start) + self.alignxc_calc(alxc_images, alxc_pos, dark_image, distanceMove, plot) + + def alignxc_calc(self, alxc_images, alxc_pos, dark_image, distanceMove, plot=False): + + # substract dark from images + for i in alxc_images: + i = i.astype(float) - dark_image.astype(float) + # convert to numpy array + al_array = np.array(alxc_images) + + # calculate XC tilt shifts + # the scan positions and the pixel_size must have the same units! + + tr_calc = DetectorTranslationAlongBeam() + # enable calculation results plotting + if plot is True: + tr_calc.verbose = True + + shifts_v, shifts_h, _ = tr_calc.find_shift( + al_array, alxc_pos, return_shifts=True, use_adjacent_imgs=True + ) + print(shifts_v, shifts_h) + pixel_size = self._pixel_size() + # pixel size should be im mm + tilt_v_deg = np.rad2deg(np.arctan(shifts_v * pixel_size)) + tilt_h_deg = np.rad2deg(np.arctan(shifts_h * pixel_size)) + print(f"\nVertical tilt in deg (thy): {tilt_v_deg}") + print(f"Horizontal tilt in deg (thz): {tilt_h_deg}\n") + + # apply to the motors + # needs a real object to configure motors!!!!! + + # Kill the plot window when it was requested + if plot is True: + if click.confirm("Close the plot window?", default=True): + # workaround for nabu plotting problem + plt.close("all") + + def alignxc_scan( + self, + xc, + start, + stop, + steps, + expo_time, + detector, + svg, + shg, + slit_gap, + save=False, + ): + + # save current slit positions + vg_pos = svg.position + hg_pos = shg.position + print(f"Current slit gap positions: vertical={vg_pos}, horizontal={hg_pos}") + print(f"Current xc positions: xc={xc.position}") + + if not save: + savingpreset = ImageNoSavingPreset(detector) + DEFAULT_CHAIN.add_preset(savingpreset) + # clean-up: move back xc motor, open_slits + restore_list = (cleanup_axis.POS,) + with error_cleanup(xc, svg, shg, restore_list=restore_list): + with capture_exceptions(raise_index=0) as capture: + with capture(): + # close the slits + umv(svg, slit_gap, shg, slit_gap) + + # scan xc + scan = ascan( + xc, + start, + stop, + steps, + expo_time, + detector.image, + title="align xc scan", + save=save, + ) + + # open the slits + umv(svg, vg_pos, shg, hg_pos) + + if not save: + DEFAULT_CHAIN.remove_preset(savingpreset) + # return images + return scan + + # test if an error has occured + if len(capture.failed) > 0: + if not save: + DEFAULT_CHAIN.remove_preset(savingpreset) + + return None + + +class ImageNoSavingPreset(ChainPreset): + def __init__(self, detector): + self._images = [] + self._detector = detector + + class Iterator(ChainIterationPreset): + def __init__(self, images, detector): + self._images = images + self._detector = detector + + def prepare(self): + pass + + def start(self): + pass + + def stop(self): + self._images.append(image_from_server(self._detector.proxy, -1).array) + + def get_iterator(self, acq_chain): + while True: + yield ImageNoSavingPreset.Iterator(self._images, self._detector) + + def _get_images(self): + return self._images diff --git a/tomo/optic/align.py b/tomo/optic/align.py index 2a70cbdf..5c06d493 100644 --- a/tomo/optic/align.py +++ b/tomo/optic/align.py @@ -1,820 +1,3 @@ -import numpy as np -import click -import gevent -from bliss.common.scans import ascan, pointscan, DEFAULT_CHAIN -from bliss.common.scans.ct import ct -try: - # BLISS >= 2.0 - bliss = 2 - from blissdata.lima.image_utils import image_from_server -except ImportError: - # BLISS <= 1.11 - bliss = 1 - from blissdata.data.lima_image import image_from_server +# Compatibility with tomo <= 2.5.0 -from bliss.common.cleanup import ( - error_cleanup, - cleanup, - capture_exceptions, - axis as cleanup_axis, -) -from bliss.common.utils import BOLD - -from matplotlib import pyplot as plt -from bliss.scanning.scan_tools import peak -from bliss.controllers.lima.roi import Roi -from bliss.shell.standard import umv, mv, umvr, flint -from bliss.scanning.chain import ChainPreset, ChainIterationPreset - -import logging - -_logger = logging.getLogger(__name__) - - -try: - from nabu.estimation.translation import DetectorTranslationAlongBeam - from nabu.estimation.focus import CameraFocus - from nabu.estimation.tilt import CameraTilt -except ImportError: - _logger.error("Error while importing nabu", exc_info=True) - nabu = None - - -class Align: - def __init__(self, name, config): - self.__name = name - sequence = config.get("sequence") - - self.__config = config - - self.sequence = sequence - self.paper = self.__config.get("paper") - - self.slits_vgap = self.__config.get("slits_vgap") - self.slits_hgap = self.__config.get("slits_hgap") - self.detector_vtilt = self.__config.get("detector_vtilt") - self.detector_htilt = self.__config.get("detector_htilt") - - @property - def name(self): - """ - A unique name for the Bliss object - """ - return self.__name - - @property - def config(self): - """ - The configuration for Tomo alignment - """ - return self.__config - - def __info__(self): - info_str = f"{self.name} info:\n" - if self.sequence is not None: - info_str += f" tomo = {self.sequence.name} \n" - info_str += f" detector = {self.sequence._detector.name}\n" - info_str += f" optic = {self.sequence._detectors.get_optic(self.sequence._detector).description}\n" - info_str += f" pixel size = {self._pixel_size()} \n" - info_str += f" exposure time = {self.sequence.pars.exposure_time} \n\n" - - info_str += f" focus motor = {self.sequence._detectors.get_optic(self.sequence._detector).focus_motor.name} \n" - info_str += f" rotc motor = {self.sequence._detectors.get_optic(self.sequence._detector).rotc_motor.name} \n" - info_str += f" lateral motor = {self.sequence._y_axis.name} \n\n" - else: - info_str += " tomo = No Tomo object configured!\n" - if self.paper is not None: - info_str += f" alignment paper = {self.paper.name} \n" - else: - info_str += " alignment paper = No alignment pager object configured!\n" - if self.slits_vgap is not None: - info_str += f" slits vgap = {self.slits_vgap.name} \n" - else: - info_str += " slits vgap = No slits vgap object configured!\n" - if self.slits_hgap is not None: - info_str += f" slits hgap = {self.slits_hgap.name} \n" - else: - info_str += " slits hgap = No slits hgap object configured!\n" - if self.detector_vtilt is not None: - info_str += f" detector vtilt = {self.detector_vtilt.name} \n" - else: - info_str += " detector vtilt = No detector vtilt object configured!\n" - if self.detector_htilt is not None: - info_str += f" detector htilt = {self.detector_htilt.name} \n" - else: - info_str += " detector htilt = No detector htilt object configured!\n" - return info_str - - def _pixel_size(self): - # pixel size returned by tomo is in um - pixel_size = self.sequence._detectors.get_image_pixel_size( - self.sequence._detector - ) - - # pixel size for Nabu caluculations must be in mm - pixel_size = pixel_size / 1000.0 - print(BOLD("\npixel_size in mm = %g\n" % pixel_size)) - - return pixel_size - - def _get_images_bliss_1_11(self, scan, detector): - """Compatibility with BLISS 1.11""" - from blissdata.data.events import ImageNotSaved - - scan_images = [] - lima_data = scan.get_data()[detector.image] - npoints = lima_data.last_index - - if npoints < lima_data.buffer_max_number: - try: - for i in range(npoints): - scan_images.append(lima_data.get_image(i)) - - return scan_images - except ImageNotSaved: - chain_presets = next(scan.acq_chain._presets_master_list.values()) - for preset in chain_presets: - if isinstance(preset, ImageNoSavingPreset): - scan_images = preset._get_images() - return scan_images - raise - else: - ValueError("Cannot read images! Not enough images in the Lima image buffer") - - def _get_images(self, scan, detector): - # read back the scan images aquired - if bliss == 1: - return self._get_images_bliss_1_11(scan, detector) - - if scan.scan_info["save"]: - lima_data = scan.streams[detector.image] - return lima_data[:] - elif 'flat' in scan.name or 'dark' in scan.name: - lima_data = list(image_from_server(detector.proxy, -1).array) - return lima_data - else: - chain_presets = next(scan.acq_chain._presets_master_list.values()) - for preset in chain_presets: - if isinstance(preset, ImageNoSavingPreset): - lima_data = preset._get_images() - return lima_data - - # Detector focus alignment - # - - def focus(self, save=False, plot=False): - - # prepare the focus scan parameters - scan_pars = self.sequence._detectors.get_optic( - self.sequence._detector - ).focus_scan_parameters() - - scan_range = scan_pars["focus_scan_range"] - scan_steps = scan_pars["focus_scan_steps"] - focus_type = scan_pars["focus_type"] - focus_motor = self.sequence._detectors.get_optic( - self.sequence._detector - ).focus_motor - - if ( - focus_type == "Rotation" - and self.sequence._detectors.get_optic( - self.sequence._detector - ).magnification - < 0.7 - ): - scan_range /= 2 - scan_steps *= 2 - - start = focus_motor.position - scan_range - stop = focus_motor.position + scan_range - - # pixel size in mm - pixel_size = self._pixel_size() - - if self.paper: - # move in paper - print("Move in paper") - self.paper.IN() - - # Do the focus scan - focus_scan = self.focus_scan( - focus_motor, - start, - stop, - scan_steps, - self.sequence.pars.exposure_time, - self.sequence._detector, - save=save, - ) - - if focus_scan is not None: - self.focus_calc( - focus_scan, - focus_motor, - self.sequence._detector, - pixel_size, - plot, - ) - - ct(self.sequence.pars.exposure_time) - print(f"Focus motor position = {focus_motor.position}") - - # Move out paper - if self.paper and click.confirm("Move out paper?", default=True): - self.paper.OUT() - - def focus_calc( - self, - focus_scan, - focus_motor, - detector, - pixel_size, - plot=False, - ): - if focus_scan is not None: - # read back the aquired images - foc_images = self._get_images(focus_scan, detector) - # convert to numpy array - foc_array = np.array(foc_images) - - # read back the aquired positions - foc_pos = focus_scan.get_data()[focus_motor.name] - - focus_calc = CameraFocus() - # enable calculation results plotting - if plot is True: - focus_calc.verbose = True - - focus_pos, focus_ind, tilts_vh = focus_calc.find_scintillator_tilt( - foc_array, foc_pos - ) - # print (focus_pos, focus_ind, tilts_vh) - - # tilts_corr_vh_deg = -np.rad2deg(np.arctan(tilts_vh / pixel_size)) - tilts_corr_vh_rad = - tilts_vh / pixel_size - - print(f"\nBest focus position: {focus_pos}") - print(f"Tilts vert. and hor. in rad: {tilts_corr_vh_rad}\n") - - if plot is True: - pause = plt.pause(0.1) - g = gevent.spawn(pause) - if click.confirm("Move to the best focus position?", default=True): - # Always move to the best focus from the bottom - mv(focus_motor, foc_pos[0]) - # move focus motor to maximum - mv(focus_motor, focus_pos) - - # Kill the plot window when it was requested - if plot is True: - g.kill() - # workaround for nabu plotting problem - plt.close("all") - - def focus_scan( - self, - focus_motor, - start, - stop, - steps, - expo_time, - detector, - save=False, - ): - # prepare roi counters for statistics - focus_roi = Roi(0, 0, detector.image.roi[2], detector.image.roi[3]) - - det_rois = detector.roi_counters - det_rois["focus_roi"] = focus_roi - - std = detector.roi_counters.counters.focus_roi_std - counters = (std, detector.image) - - if not save: - savingpreset = ImageNoSavingPreset(detector) - DEFAULT_CHAIN.add_preset(savingpreset) - # clean-up: move back focus motor, delete roi counters, move out paper - restore_list = (cleanup_axis.POS,) - with cleanup(*[focus_motor], restore_list=restore_list): - with capture_exceptions(raise_index=0) as capture: - with capture(): - # execute focus scan - scan = ascan( - focus_motor, - start, - stop, - steps, - expo_time, - counters, - title="focus scan", - save=save, - ) - - # get the position on the maximum of the standard deviation - pos_std_max = peak() - - # delete roi counters - del det_rois["focus_roi"] - - if not save: - DEFAULT_CHAIN.remove_preset(savingpreset) - - return scan - - # test if an error has occured - if len(capture.failed) > 0: - # delete roi counters - del det_rois["focus_roi"] - - if not save: - DEFAULT_CHAIN.remove_preset(savingpreset) - - return None - - # Lateral alignment and camera tilt - - def align(self, save=False, plot=False, plot_images=False, use_fft_polar_method=False, questions_move=True): - # move rotation axis to 0 - umv(self.sequence._rotation_axis, 0) - - # take reference image - flat_n = 1 - flat_runner = self.sequence._tomo_runners["flat"] - flat_scan = flat_runner( - self.sequence.pars.exposure_time, - flat_n, - self.sequence._detector, - projection=self.sequence._inpars.projection, - save=save, - ) - flat_images = self._get_images( - flat_scan, self.sequence._detector - ) - flat_image = flat_images[-1] - - # take dark image - dark_n = 1 - dark_runner = self.sequence._tomo_runners["dark"] - dark_scan = dark_runner( - self.sequence.pars.exposure_time, dark_n, self.sequence._detector, save=save - ) - # read back the aquired images - dark_images = self._get_images( - dark_scan, self.sequence._detector - ) - dark_image = dark_images[-1] - - # pixel size in mm - pixel_size = self._pixel_size() - # do alignment scan - al_scan = self.align_scan( - self.sequence._rotation_axis, - self.sequence.pars.exposure_time, - self.sequence._detector, - save=save, - ) - - # read back the aquired images - al_images = self._get_images(al_scan, self.sequence._detector) - - # works only with on motor today! - lateral_motor = self.sequence._y_axis - rotc = self.sequence._detectors.get_optic(self.sequence._detector).rotc_motor - - source_sample_distance = self.sequence._tomo_config.source_sample_distance - sample_detector_distance = self.sequence._detectors.get_tomo_detector( - self.sequence._detector - ).sample_detector_distance - - lat_corr, camera_tilt = self.align_calc( - al_images, - dark_image, - flat_image, - lateral_motor, - rotc, - self.sequence._detector, - pixel_size, - source_sample_distance, - sample_detector_distance, - plot, - plot_images, - use_fft_polar_method, - questions_move, - ) - - ct(self.sequence.pars.exposure_time) - print(f"Lateral alignment motor position = {lateral_motor.position}") - if rotc is not None: - print(f"Camera tilt position {rotc.name} = {rotc.position}") - - return lat_corr, camera_tilt - - def correct_movement(self, d_t0, d_t1): - if abs((d_t0 - d_t1) / d_t0) > 0.05: - flag_not_moving = False - sign = ((d_t0 < 0) == (d_t1 < 0)) - if sign: - if abs(d_t0) < abs(d_t1): - dire = -1 - else: - dire = 1 - else: - dire = 1 - - value_corrected = d_t1 - d_t0 - fraction_correct = abs(d_t0/value_corrected)*dire - new_corection = d_t1*fraction_correct - else: - fraction_correct = 1 - flag_not_moving = True - new_corection = 0 - - return new_corection, fraction_correct, flag_not_moving - - def align_iter(self, yrot_precision=0.01, rotc_precision=0.001): - lateral_motor = self.sequence._y_axis - rotc = self.sequence._detectors.get_optic(self.sequence._detector).rotc_motor - - dyrot_t0, drotc_t0 = self.align(questions_move=False) - if (abs(dyrot_t0) < yrot_precision) and (abs(dyrot_t0) < rotc_precision): - flag_pass = True - else: - flag_pass = False - - if not flag_pass: - - umvr(lateral_motor, dyrot_t0, rotc, drotc_t0) - - i = 0 - f_rotc = 1.0 - f_yrot = 1.0 - - while True: - - dyrot_t1, drotc_t1 = self.align(questions_move=False) - - if (abs(dyrot_t1) > yrot_precision) and (abs(drotc_t1) > rotc_precision): - umvr(lateral_motor, dyrot_t1, rotc, drotc_t1) - elif (abs(dyrot_t1) > yrot_precision): - umvr(lateral_motor, dyrot_t1) - elif (abs(drotc_t1) > rotc_precision): - umvr(rotc, drotc_t1) - else: - break - - dyrot_t1 = f_yrot * dyrot_t1 - drotc_t1 = f_rotc * drotc_t1 - - dyrot_t1, f_yrot, flag_not_moving_yrot = self.correct_movement(dyrot_t0, dyrot_t1) - drotc_t1, f_rotc, flag_not_moving_rotc = self.correct_movement(drotc_t0, drotc_t1) - - dyrot_t0 = dyrot_t1 - drotc_t0 = drotc_t1 - - i += 1 - - def align_calc( - self, - al_images, - dark_image, - flat_image, - lateral_motor, - rotc_motor, - detector, - pixel_size, - source_sample_distance, - sample_detector_distance, - plot=False, - plot_images=False, - use_fft_polar_method=False, - questions_move=True, - ): - radio0 = (al_images[0].astype(float) - dark_image.astype(float)) / ( - flat_image.astype(float) - dark_image.astype(float) - ) - radio180 = (al_images[1].astype(float) - dark_image.astype(float)) / ( - flat_image.astype(float) - dark_image.astype(float) - ) - - # flip the radio180 for the calculation - radio180_flip = np.fliplr(radio180.copy()) - - if plot_images: - f = flint() - p = f.get_plot( - "image", - name="dark_image", - unique_name="dark_image", - selected=True, - closeable=True, - ) - - p.set_data(dark_image) - p.yaxis_direction = "down" - p.side_histogram_displayed = False - - p = f.get_plot( - "image", - name="flat_image", - unique_name="flat_image", - selected=True, - closeable=True, - ) - - p.set_data(flat_image) - p.yaxis_direction = "down" - p.side_histogram_displayed = False - - p = f.get_plot( - "image", - name="projection_image_0", - unique_name="projection_image_0", - selected=True, - closeable=True, - ) - - p.set_data(radio0) - p.yaxis_direction = "down" - p.side_histogram_displayed = False - - p = f.get_plot( - "image", - name="projection_image_180_flip", - unique_name="projection_image_180_flip", - selected=True, - closeable=True, - ) - - p.set_data(radio180_flip) - p.yaxis_direction = "down" - p.side_histogram_displayed = False - - # plot_image(radio0) - # plot_image(radio180_flip) - - # calculate the lateral correction for the rotation axis - # and the camera tilt with line by line correlation - tilt_calc = CameraTilt() - # enable calculation results plotting - if plot is True: - tilt_calc.verbose = True - - method = "1d-correlation" - if use_fft_polar_method: - method = "fft-polar" - pixel_cor, camera_tilt = tilt_calc.compute_angle( - radio0, radio180_flip, method=method - ) - print("CameraTilt: pixel_cor = %f" % pixel_cor) - - # distance from source to sample = L1(mm) - # distance from source to detector = L2(mm) - # size of the image pixel = s(mm/pixel) - # dmot(mm) = L1/L2*s*pixel_cor - # cor_factor = ( - # source_sample_distance - # / (source_sample_distance + sample_detector_distance) - # * pixel_size - # ) - - pos_cor = pixel_size * pixel_cor - - # pixel size in mm - pixel_size = self._pixel_size() - print(BOLD(f"Lateral alignment position correction in mm: {-pos_cor:>10.5f}")) - print( - BOLD( - f"Camera tilt in deg (motor rotc: {rotc_motor.name}): {camera_tilt:35.5f}\n" - ) - ) - - if plot is True: - pause = plt.pause(0.1) - g = gevent.spawn(pause) - - # if click.confirm( - # "Apply the lateral position correction and the camera tilt?", default=False): - - if questions_move: - if click.confirm("Apply the lateral position correction?", default=False): - # apply lateral correction - umvr(lateral_motor, -pos_cor) - - if click.confirm("Apply the camera tilt?", default=False): - # apply tilt correction - try: - umvr(rotc_motor, camera_tilt) - except Exception: - pass - # Kill the plot window when it was requested - if plot is True: - # workaround for nabu plotting problem - g.kill() - plt.close("all") - - return -pos_cor, camera_tilt - - def align_scan( - self, rotation_motor, expo_time, detector, save=False - ): - if not save: - savingpreset = ImageNoSavingPreset(detector) - DEFAULT_CHAIN.add_preset(savingpreset) - # clean-up: move back rot - restore_list = (cleanup_axis.POS,) - with cleanup(rotation_motor, restore_list=restore_list): - with capture_exceptions(raise_index=0) as capture: - with capture(): - # scan rot - rot_pos_list = [0.0, 180.0] - scan = pointscan( - rotation_motor, - rot_pos_list, - expo_time, - detector.image, - title="align scan", - save=save, - ) - - if not save: - DEFAULT_CHAIN.remove_preset(savingpreset) - - return scan - - # test if an error has occured - if len(capture.failed) > 0: - if not save: - DEFAULT_CHAIN.remove_preset(savingpreset) - - return None - - # Camera stage tilt alignment - - def alignxc(self, start=100, stop=800, steps=10, save=False, plot=False): - # pixel size in mm - pixel_size = self._pixel_size() - - # slit gap to use - if pixel_size < 0.003: # 3um - slit_gap = 0.2 - else: - slit_gap = 1.0 - - alxc_scan = self.alignxc_scan( - self.sequence._detector_axis, - start, - stop, - steps, - self.sequence.pars.exposure_time, - self.sequence._detector, - self.slits_vgap, - self.slits_hgap, - slit_gap, - save=save, - ) - # read back the aquired images - alxc_images = self._get_images( - alxc_scan, self.sequence._detector - ) - # print(alxc_images) - - # read back the motor positions - alxc_pos = alxc_scan.get_data()[self.sequence._detector_axis.name] - # print(al_pos) - - # take dark image - dark_n = 1 - dark_runner = self.sequence._tomo_runners["dark"] - dark_scan = dark_runner( - self.sequence.pars.exposure_time, dark_n, self.sequence._detector, save=save - ) - # read back the aquired images - dark_images = self._get_images( - dark_scan, self.sequence._detector - ) - dark_image = dark_images[0] - - distanceMove = abs(stop - start) - self.alignxc_calc(alxc_images, alxc_pos, dark_image, distanceMove, plot) - - def alignxc_calc(self, alxc_images, alxc_pos, dark_image, distanceMove, plot=False): - - # substract dark from images - for i in alxc_images: - i = i.astype(float) - dark_image.astype(float) - # convert to numpy array - al_array = np.array(alxc_images) - - # calculate XC tilt shifts - # the scan positions and the pixel_size must have the same units! - - tr_calc = DetectorTranslationAlongBeam() - # enable calculation results plotting - if plot is True: - tr_calc.verbose = True - - shifts_v, shifts_h, _ = tr_calc.find_shift( - al_array, alxc_pos, return_shifts=True, use_adjacent_imgs=True - ) - print(shifts_v, shifts_h) - pixel_size = self._pixel_size() - # pixel size should be im mm - tilt_v_deg = np.rad2deg(np.arctan(shifts_v * pixel_size)) - tilt_h_deg = np.rad2deg(np.arctan(shifts_h * pixel_size)) - print(f"\nVertical tilt in deg (thy): {tilt_v_deg}") - print(f"Horizontal tilt in deg (thz): {tilt_h_deg}\n") - - # apply to the motors - # needs a real object to configure motors!!!!! - - # Kill the plot window when it was requested - if plot is True: - if click.confirm("Close the plot window?", default=True): - # workaround for nabu plotting problem - plt.close("all") - - def alignxc_scan( - self, - xc, - start, - stop, - steps, - expo_time, - detector, - svg, - shg, - slit_gap, - save=False, - ): - - # save current slit positions - vg_pos = svg.position - hg_pos = shg.position - print(f"Current slit gap positions: vertical={vg_pos}, horizontal={hg_pos}") - print(f"Current xc positions: xc={xc.position}") - - if not save: - savingpreset = ImageNoSavingPreset(detector) - DEFAULT_CHAIN.add_preset(savingpreset) - # clean-up: move back xc motor, open_slits - restore_list = (cleanup_axis.POS,) - with error_cleanup(xc, svg, shg, restore_list=restore_list): - with capture_exceptions(raise_index=0) as capture: - with capture(): - # close the slits - umv(svg, slit_gap, shg, slit_gap) - - # scan xc - scan = ascan( - xc, - start, - stop, - steps, - expo_time, - detector.image, - title="align xc scan", - save=save, - ) - - # open the slits - umv(svg, vg_pos, shg, hg_pos) - - if not save: - DEFAULT_CHAIN.remove_preset(savingpreset) - # return images - return scan - - # test if an error has occured - if len(capture.failed) > 0: - if not save: - DEFAULT_CHAIN.remove_preset(savingpreset) - - return None - - -class ImageNoSavingPreset(ChainPreset): - def __init__(self, detector): - self._images = [] - self._detector = detector - - class Iterator(ChainIterationPreset): - def __init__(self, images, detector): - self._images = images - self._detector = detector - - def prepare(self): - pass - - def start(self): - pass - - def stop(self): - self._images.append(image_from_server(self._detector.proxy, -1).array) - - def get_iterator(self, acq_chain): - while True: - yield ImageNoSavingPreset.Iterator(self._images, self._detector) - - def _get_images(self): - return self._images +from tomo.align import * # noqa -- GitLab From 94abfcc3b308cc891a3ec4a16d68287a207c42b7 Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Tue, 19 Mar 2024 11:56:00 +0100 Subject: [PATCH 02/21] Replace click by bliss.getval --- tomo/align.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tomo/align.py b/tomo/align.py index d565af22..aca701c2 100644 --- a/tomo/align.py +++ b/tomo/align.py @@ -1,8 +1,8 @@ import numpy as np -import click import gevent from bliss.common.scans import ascan, pointscan, DEFAULT_CHAIN from bliss.common.scans.ct import ct +from bliss.shell.getval import getval_yes_no try: # BLISS >= 2.0 bliss = 2 @@ -222,7 +222,7 @@ class Align: print(f"Focus motor position = {focus_motor.position}") # Move out paper - if self.paper and click.confirm("Move out paper?", default=True): + if self.paper and getval_yes_no("Move out paper?", default=True): self.paper.OUT() def focus_calc( @@ -261,7 +261,7 @@ class Align: if plot is True: pause = plt.pause(0.1) g = gevent.spawn(pause) - if click.confirm("Move to the best focus position?", default=True): + if getval_yes_no("Move to the best focus position?", default=True): # Always move to the best focus from the bottom mv(focus_motor, foc_pos[0]) # move focus motor to maximum @@ -594,15 +594,15 @@ class Align: pause = plt.pause(0.1) g = gevent.spawn(pause) - # if click.confirm( + # if getval_yes_no( # "Apply the lateral position correction and the camera tilt?", default=False): if questions_move: - if click.confirm("Apply the lateral position correction?", default=False): + if getval_yes_no("Apply the lateral position correction?", default=False): # apply lateral correction umvr(lateral_motor, -pos_cor) - if click.confirm("Apply the camera tilt?", default=False): + if getval_yes_no("Apply the camera tilt?", default=False): # apply tilt correction try: umvr(rotc_motor, camera_tilt) @@ -731,7 +731,7 @@ class Align: # Kill the plot window when it was requested if plot is True: - if click.confirm("Close the plot window?", default=True): + if getval_yes_no("Close the plot window?", default=True): # workaround for nabu plotting problem plt.close("all") -- GitLab From a372d36c80774dd92fc832c6108bcc130e675389 Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Tue, 19 Mar 2024 11:57:09 +0100 Subject: [PATCH 03/21] Typo blank line --- tomo/align.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tomo/align.py b/tomo/align.py index aca701c2..c0547788 100644 --- a/tomo/align.py +++ b/tomo/align.py @@ -3,6 +3,8 @@ import gevent from bliss.common.scans import ascan, pointscan, DEFAULT_CHAIN from bliss.common.scans.ct import ct from bliss.shell.getval import getval_yes_no + + try: # BLISS >= 2.0 bliss = 2 @@ -31,7 +33,6 @@ import logging _logger = logging.getLogger(__name__) - try: from nabu.estimation.translation import DetectorTranslationAlongBeam from nabu.estimation.focus import CameraFocus @@ -164,7 +165,6 @@ class Align: # def focus(self, save=False, plot=False): - # prepare the focus scan parameters scan_pars = self.sequence._detectors.get_optic( self.sequence._detector @@ -444,7 +444,6 @@ class Align: flag_pass = False if not flag_pass: - umvr(lateral_motor, dyrot_t0, rotc, drotc_t0) i = 0 @@ -452,7 +451,6 @@ class Align: f_yrot = 1.0 while True: - dyrot_t1, drotc_t1 = self.align(questions_move=False) if (abs(dyrot_t1) > yrot_precision) and (abs(drotc_t1) > rotc_precision): @@ -700,7 +698,6 @@ class Align: self.alignxc_calc(alxc_images, alxc_pos, dark_image, distanceMove, plot) def alignxc_calc(self, alxc_images, alxc_pos, dark_image, distanceMove, plot=False): - # substract dark from images for i in alxc_images: i = i.astype(float) - dark_image.astype(float) @@ -748,7 +745,6 @@ class Align: slit_gap, save=False, ): - # save current slit positions vg_pos = svg.position hg_pos = shg.position -- GitLab From a49798e3c19b764f1b2536be505284f1e8cc5d37 Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Tue, 19 Mar 2024 11:57:21 +0100 Subject: [PATCH 04/21] Typo black --- tomo/align.py | 55 +++++++++++++++++++++++++++------------------------ 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/tomo/align.py b/tomo/align.py index c0547788..56e7783d 100644 --- a/tomo/align.py +++ b/tomo/align.py @@ -151,7 +151,7 @@ class Align: if scan.scan_info["save"]: lima_data = scan.streams[detector.image] return lima_data[:] - elif 'flat' in scan.name or 'dark' in scan.name: + elif "flat" in scan.name or "dark" in scan.name: lima_data = list(image_from_server(detector.proxy, -1).array) return lima_data else: @@ -253,7 +253,7 @@ class Align: # print (focus_pos, focus_ind, tilts_vh) # tilts_corr_vh_deg = -np.rad2deg(np.arctan(tilts_vh / pixel_size)) - tilts_corr_vh_rad = - tilts_vh / pixel_size + tilts_corr_vh_rad = -tilts_vh / pixel_size print(f"\nBest focus position: {focus_pos}") print(f"Tilts vert. and hor. in rad: {tilts_corr_vh_rad}\n") @@ -335,7 +335,14 @@ class Align: # Lateral alignment and camera tilt - def align(self, save=False, plot=False, plot_images=False, use_fft_polar_method=False, questions_move=True): + def align( + self, + save=False, + plot=False, + plot_images=False, + use_fft_polar_method=False, + questions_move=True, + ): # move rotation axis to 0 umv(self.sequence._rotation_axis, 0) @@ -349,9 +356,7 @@ class Align: projection=self.sequence._inpars.projection, save=save, ) - flat_images = self._get_images( - flat_scan, self.sequence._detector - ) + flat_images = self._get_images(flat_scan, self.sequence._detector) flat_image = flat_images[-1] # take dark image @@ -361,9 +366,7 @@ class Align: self.sequence.pars.exposure_time, dark_n, self.sequence._detector, save=save ) # read back the aquired images - dark_images = self._get_images( - dark_scan, self.sequence._detector - ) + dark_images = self._get_images(dark_scan, self.sequence._detector) dark_image = dark_images[-1] # pixel size in mm @@ -414,7 +417,7 @@ class Align: def correct_movement(self, d_t0, d_t1): if abs((d_t0 - d_t1) / d_t0) > 0.05: flag_not_moving = False - sign = ((d_t0 < 0) == (d_t1 < 0)) + sign = (d_t0 < 0) == (d_t1 < 0) if sign: if abs(d_t0) < abs(d_t1): dire = -1 @@ -424,8 +427,8 @@ class Align: dire = 1 value_corrected = d_t1 - d_t0 - fraction_correct = abs(d_t0/value_corrected)*dire - new_corection = d_t1*fraction_correct + fraction_correct = abs(d_t0 / value_corrected) * dire + new_corection = d_t1 * fraction_correct else: fraction_correct = 1 flag_not_moving = True @@ -453,11 +456,13 @@ class Align: while True: dyrot_t1, drotc_t1 = self.align(questions_move=False) - if (abs(dyrot_t1) > yrot_precision) and (abs(drotc_t1) > rotc_precision): + if (abs(dyrot_t1) > yrot_precision) and ( + abs(drotc_t1) > rotc_precision + ): umvr(lateral_motor, dyrot_t1, rotc, drotc_t1) - elif (abs(dyrot_t1) > yrot_precision): + elif abs(dyrot_t1) > yrot_precision: umvr(lateral_motor, dyrot_t1) - elif (abs(drotc_t1) > rotc_precision): + elif abs(drotc_t1) > rotc_precision: umvr(rotc, drotc_t1) else: break @@ -465,8 +470,12 @@ class Align: dyrot_t1 = f_yrot * dyrot_t1 drotc_t1 = f_rotc * drotc_t1 - dyrot_t1, f_yrot, flag_not_moving_yrot = self.correct_movement(dyrot_t0, dyrot_t1) - drotc_t1, f_rotc, flag_not_moving_rotc = self.correct_movement(drotc_t0, drotc_t1) + dyrot_t1, f_yrot, flag_not_moving_yrot = self.correct_movement( + dyrot_t0, dyrot_t1 + ) + drotc_t1, f_rotc, flag_not_moving_rotc = self.correct_movement( + drotc_t0, drotc_t1 + ) dyrot_t0 = dyrot_t1 drotc_t0 = drotc_t1 @@ -614,9 +623,7 @@ class Align: return -pos_cor, camera_tilt - def align_scan( - self, rotation_motor, expo_time, detector, save=False - ): + def align_scan(self, rotation_motor, expo_time, detector, save=False): if not save: savingpreset = ImageNoSavingPreset(detector) DEFAULT_CHAIN.add_preset(savingpreset) @@ -673,9 +680,7 @@ class Align: save=save, ) # read back the aquired images - alxc_images = self._get_images( - alxc_scan, self.sequence._detector - ) + alxc_images = self._get_images(alxc_scan, self.sequence._detector) # print(alxc_images) # read back the motor positions @@ -689,9 +694,7 @@ class Align: self.sequence.pars.exposure_time, dark_n, self.sequence._detector, save=save ) # read back the aquired images - dark_images = self._get_images( - dark_scan, self.sequence._detector - ) + dark_images = self._get_images(dark_scan, self.sequence._detector) dark_image = dark_images[0] distanceMove = abs(stop - start) -- GitLab From d3b06731e71d5ef8e19b35a61c87a4cda05bca48 Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Tue, 19 Mar 2024 13:46:59 +0100 Subject: [PATCH 05/21] pos_std_max is unused --- tomo/align.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tomo/align.py b/tomo/align.py index 56e7783d..06488d43 100644 --- a/tomo/align.py +++ b/tomo/align.py @@ -23,7 +23,6 @@ from bliss.common.cleanup import ( from bliss.common.utils import BOLD from matplotlib import pyplot as plt -from bliss.scanning.scan_tools import peak from bliss.controllers.lima.roi import Roi from bliss.shell.standard import umv, mv, umvr, flint from bliss.scanning.chain import ChainPreset, ChainIterationPreset @@ -313,7 +312,8 @@ class Align: ) # get the position on the maximum of the standard deviation - pos_std_max = peak() + # from bliss.scanning.scan_tools import peak + # pos_std_max = peak() # delete roi counters del det_rois["focus_roi"] -- GitLab From b252242568ce3e9ca8484e78e0a7a92e03e3d3a0 Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Tue, 19 Mar 2024 17:11:07 +0100 Subject: [PATCH 06/21] Rework the code with typing and tomo_config --- tomo/align.py | 177 +++++++++++++++++++++++++++++++------------------- 1 file changed, 110 insertions(+), 67 deletions(-) diff --git a/tomo/align.py b/tomo/align.py index 06488d43..1128e227 100644 --- a/tomo/align.py +++ b/tomo/align.py @@ -1,10 +1,13 @@ +from __future__ import annotations + +import typing +import types import numpy as np import gevent from bliss.common.scans import ascan, pointscan, DEFAULT_CHAIN from bliss.common.scans.ct import ct from bliss.shell.getval import getval_yes_no - try: # BLISS >= 2.0 bliss = 2 @@ -26,12 +29,20 @@ from matplotlib import pyplot as plt from bliss.controllers.lima.roi import Roi from bliss.shell.standard import umv, mv, umvr, flint from bliss.scanning.chain import ChainPreset, ChainIterationPreset +from bliss.controllers.multiplepositions import MultiplePositions +from tomo.fulltomo import FullFieldTomo +from tomo.controllers.tomo_config import TomoConfig + +if typing.TYPE_CHECKING: + from bliss.common.axis import Axis + from bliss.controllers.lima.lima_base import Lima import logging _logger = logging.getLogger(__name__) +nabu: types.ModuleType | None try: from nabu.estimation.translation import DetectorTranslationAlongBeam from nabu.estimation.focus import CameraFocus @@ -42,46 +53,73 @@ except ImportError: class Align: - def __init__(self, name, config): + """ + Procedure to align the rotation stage. + """ + + def __init__(self, name: str, config: dict): self.__name = name sequence = config.get("sequence") + if not isinstance(sequence, FullFieldTomo): + raise TypeError(f"Expected FullFieldTomo but found {type(sequence)}") + + tomo_config = sequence.tomo_config + if not isinstance(tomo_config, TomoConfig): + raise TypeError(f"Expected TomoConfig but found {type(tomo_config)}") + + paper = config.get("paper") + if paper is not None: + if not isinstance(paper, MultiplePositions): + raise TypeError( + f"Expected paper as MultiplePositions but found {type(paper)}" + ) + if "IN" not in paper.targets_dict: + raise RuntimeError("Paper {paper.name} must have a IN position") + if "OUT" not in paper.targets_dict: + raise RuntimeError("Paper {paper.name} must have a OUT position") - self.__config = config - - self.sequence = sequence - self.paper = self.__config.get("paper") + self.__config: dict = config + self._tomo_config: TomoConfig = tomo_config + self.sequence: FullFieldTomo = sequence + self.paper: MultiplePositions | None = paper - self.slits_vgap = self.__config.get("slits_vgap") - self.slits_hgap = self.__config.get("slits_hgap") - self.detector_vtilt = self.__config.get("detector_vtilt") - self.detector_htilt = self.__config.get("detector_htilt") + self.slits_vgap: Axis | None = self.__config.get("slits_vgap") + self.slits_hgap: Axis | None = self.__config.get("slits_hgap") + self.detector_vtilt: Axis | None = self.__config.get("detector_vtilt") + self.detector_htilt: Axis | None = self.__config.get("detector_htilt") @property - def name(self): + def name(self) -> str: """ A unique name for the Bliss object """ return self.__name @property - def config(self): + def config(self) -> dict: """ The configuration for Tomo alignment """ return self.__config - def __info__(self): + def __info__(self) -> str: info_str = f"{self.name} info:\n" if self.sequence is not None: + detector = self.sequence._detector + if detector is None: + raise RuntimeError("No detector defined") + optic = self._tomo_config.detectors.get_optic(detector) info_str += f" tomo = {self.sequence.name} \n" - info_str += f" detector = {self.sequence._detector.name}\n" - info_str += f" optic = {self.sequence._detectors.get_optic(self.sequence._detector).description}\n" + info_str += f" detector = {detector.name}\n" + info_str += f" optic = {optic.description}\n" info_str += f" pixel size = {self._pixel_size()} \n" - info_str += f" exposure time = {self.sequence.pars.exposure_time} \n\n" + info_str += ( + f" exposure time = {self._tomo_config.pars.exposure_time} \n\n" + ) - info_str += f" focus motor = {self.sequence._detectors.get_optic(self.sequence._detector).focus_motor.name} \n" - info_str += f" rotc motor = {self.sequence._detectors.get_optic(self.sequence._detector).rotc_motor.name} \n" - info_str += f" lateral motor = {self.sequence._y_axis.name} \n\n" + info_str += f" focus motor = {optic.focus_motor.name} \n" + info_str += f" rotc motor = {optic.rotc_motor.name} \n" + info_str += f" lateral motor = {self._tomo_config._y_axis.name} \n\n" else: info_str += " tomo = No Tomo object configured!\n" if self.paper is not None: @@ -108,7 +146,7 @@ class Align: def _pixel_size(self): # pixel size returned by tomo is in um - pixel_size = self.sequence._detectors.get_image_pixel_size( + pixel_size = self._tomo_config.detectors.get_image_pixel_size( self.sequence._detector ) @@ -165,24 +203,15 @@ class Align: def focus(self, save=False, plot=False): # prepare the focus scan parameters - scan_pars = self.sequence._detectors.get_optic( - self.sequence._detector - ).focus_scan_parameters() + optic = self._tomo_config.detectors.get_optic(self.sequence._detector) + scan_pars = optic.focus_scan_parameters() scan_range = scan_pars["focus_scan_range"] scan_steps = scan_pars["focus_scan_steps"] focus_type = scan_pars["focus_type"] - focus_motor = self.sequence._detectors.get_optic( - self.sequence._detector - ).focus_motor - - if ( - focus_type == "Rotation" - and self.sequence._detectors.get_optic( - self.sequence._detector - ).magnification - < 0.7 - ): + focus_motor = optic.focus_motor + + if focus_type == "Rotation" and optic.magnification < 0.7: scan_range /= 2 scan_steps *= 2 @@ -203,7 +232,7 @@ class Align: start, stop, scan_steps, - self.sequence.pars.exposure_time, + self._tomo_config.pars.exposure_time, self.sequence._detector, save=save, ) @@ -217,7 +246,7 @@ class Align: plot, ) - ct(self.sequence.pars.exposure_time) + ct(self._tomo_config.pars.exposure_time) print(f"Focus motor position = {focus_motor.position}") # Move out paper @@ -337,59 +366,67 @@ class Align: def align( self, - save=False, - plot=False, - plot_images=False, - use_fft_polar_method=False, - questions_move=True, + save: bool = False, + plot: bool = False, + plot_images: bool = False, + use_fft_polar_method: bool = False, + questions_move: bool = True, ): + tomo_config = self._tomo_config + exposure_time = tomo_config.pars.exposure_time + detector = self.sequence._detector + optic = tomo_config.detectors.get_optic(detector) + rotation_axis = tomo_config.rotation_axis + # move rotation axis to 0 - umv(self.sequence._rotation_axis, 0) + umv(rotation_axis, 0) # take reference image flat_n = 1 - flat_runner = self.sequence._tomo_runners["flat"] + flat_runner = tomo_config.get_runner("flat") flat_scan = flat_runner( - self.sequence.pars.exposure_time, + exposure_time, flat_n, - self.sequence._detector, + detector, projection=self.sequence._inpars.projection, save=save, ) - flat_images = self._get_images(flat_scan, self.sequence._detector) + flat_images = self._get_images(flat_scan, detector) flat_image = flat_images[-1] # take dark image dark_n = 1 - dark_runner = self.sequence._tomo_runners["dark"] + dark_runner = tomo_config.get_runner("dark") dark_scan = dark_runner( - self.sequence.pars.exposure_time, dark_n, self.sequence._detector, save=save + exposure_time, + dark_n, + detector, + save=save, ) # read back the aquired images - dark_images = self._get_images(dark_scan, self.sequence._detector) + dark_images = self._get_images(dark_scan, detector) dark_image = dark_images[-1] # pixel size in mm pixel_size = self._pixel_size() # do alignment scan al_scan = self.align_scan( - self.sequence._rotation_axis, - self.sequence.pars.exposure_time, - self.sequence._detector, + rotation_axis, + exposure_time, + detector, save=save, ) # read back the aquired images - al_images = self._get_images(al_scan, self.sequence._detector) + al_images = self._get_images(al_scan, detector) # works only with on motor today! - lateral_motor = self.sequence._y_axis - rotc = self.sequence._detectors.get_optic(self.sequence._detector).rotc_motor + lateral_motor = tomo_config.y_axis + rotc = optic.rotc_motor - source_sample_distance = self.sequence._tomo_config.source_sample_distance - sample_detector_distance = self.sequence._detectors.get_tomo_detector( - self.sequence._detector - ).sample_detector_distance + tomo_detector = tomo_config.detectors.get_tomo_detector(detector) + source_sample_distance = tomo_config.source_sample_distance + sample_detector_distance = tomo_detector.sample_detector_distance lat_corr, camera_tilt = self.align_calc( al_images, @@ -397,7 +434,7 @@ class Align: flat_image, lateral_motor, rotc, - self.sequence._detector, + detector, pixel_size, source_sample_distance, sample_detector_distance, @@ -407,7 +444,7 @@ class Align: questions_move, ) - ct(self.sequence.pars.exposure_time) + ct(exposure_time) print(f"Lateral alignment motor position = {lateral_motor.position}") if rotc is not None: print(f"Camera tilt position {rotc.name} = {rotc.position}") @@ -438,7 +475,8 @@ class Align: def align_iter(self, yrot_precision=0.01, rotc_precision=0.001): lateral_motor = self.sequence._y_axis - rotc = self.sequence._detectors.get_optic(self.sequence._detector).rotc_motor + optic = self._tomo_config.detectors.get_optic(self.sequence._detector) + rotc = optic.rotc_motor dyrot_t0, drotc_t0 = self.align(questions_move=False) if (abs(dyrot_t0) < yrot_precision) and (abs(dyrot_t0) < rotc_precision): @@ -623,7 +661,9 @@ class Align: return -pos_cor, camera_tilt - def align_scan(self, rotation_motor, expo_time, detector, save=False): + def align_scan( + self, rotation_motor: Axis, expo_time: float, detector: Lima, save: bool = False + ): if not save: savingpreset = ImageNoSavingPreset(detector) DEFAULT_CHAIN.add_preset(savingpreset) @@ -672,7 +712,7 @@ class Align: start, stop, steps, - self.sequence.pars.exposure_time, + self._tomo_config.pars.exposure_time, self.sequence._detector, self.slits_vgap, self.slits_hgap, @@ -684,14 +724,17 @@ class Align: # print(alxc_images) # read back the motor positions - alxc_pos = alxc_scan.get_data()[self.sequence._detector_axis.name] + alxc_pos = alxc_scan.get_data()[self.tomo_config.detector_axis.name] # print(al_pos) # take dark image dark_n = 1 - dark_runner = self.sequence._tomo_runners["dark"] + dark_runner = self._tomo_config.get_runner("dark") dark_scan = dark_runner( - self.sequence.pars.exposure_time, dark_n, self.sequence._detector, save=save + self._tomo_config.pars.exposure_time, + dark_n, + self.sequence._detector, + save=save, ) # read back the aquired images dark_images = self._get_images(dark_scan, self.sequence._detector) -- GitLab From 0c9b32988a4a891adced71f1faa15612308452a0 Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Tue, 19 Mar 2024 18:20:59 +0100 Subject: [PATCH 07/21] Create dedicated modules for read_images and image_no_saving --- tomo/align.py | 101 +++----------------------- tomo/chain_presets/image_no_saving.py | 60 +++++++++++++++ tomo/helpers/read_images.py | 73 +++++++++++++++++++ 3 files changed, 144 insertions(+), 90 deletions(-) create mode 100644 tomo/chain_presets/image_no_saving.py create mode 100644 tomo/helpers/read_images.py diff --git a/tomo/align.py b/tomo/align.py index 1128e227..7bcf8312 100644 --- a/tomo/align.py +++ b/tomo/align.py @@ -4,19 +4,12 @@ import typing import types import numpy as np import gevent +import logging +from matplotlib import pyplot as plt + from bliss.common.scans import ascan, pointscan, DEFAULT_CHAIN from bliss.common.scans.ct import ct from bliss.shell.getval import getval_yes_no - -try: - # BLISS >= 2.0 - bliss = 2 - from blissdata.lima.image_utils import image_from_server -except ImportError: - # BLISS <= 1.11 - bliss = 1 - from blissdata.data.lima_image import image_from_server - from bliss.common.cleanup import ( error_cleanup, cleanup, @@ -24,21 +17,18 @@ from bliss.common.cleanup import ( axis as cleanup_axis, ) from bliss.common.utils import BOLD - -from matplotlib import pyplot as plt from bliss.controllers.lima.roi import Roi from bliss.shell.standard import umv, mv, umvr, flint -from bliss.scanning.chain import ChainPreset, ChainIterationPreset from bliss.controllers.multiplepositions import MultiplePositions from tomo.fulltomo import FullFieldTomo from tomo.controllers.tomo_config import TomoConfig +from tomo.chain_presets.image_no_saving import ImageNoSavingPreset +from tomo.helpers.read_images import read_images if typing.TYPE_CHECKING: from bliss.common.axis import Axis from bliss.controllers.lima.lima_base import Lima -import logging - _logger = logging.getLogger(__name__) @@ -156,48 +146,6 @@ class Align: return pixel_size - def _get_images_bliss_1_11(self, scan, detector): - """Compatibility with BLISS 1.11""" - from blissdata.data.events import ImageNotSaved - - scan_images = [] - lima_data = scan.get_data()[detector.image] - npoints = lima_data.last_index - - if npoints < lima_data.buffer_max_number: - try: - for i in range(npoints): - scan_images.append(lima_data.get_image(i)) - - return scan_images - except ImageNotSaved: - chain_presets = next(scan.acq_chain._presets_master_list.values()) - for preset in chain_presets: - if isinstance(preset, ImageNoSavingPreset): - scan_images = preset._get_images() - return scan_images - raise - else: - ValueError("Cannot read images! Not enough images in the Lima image buffer") - - def _get_images(self, scan, detector): - # read back the scan images aquired - if bliss == 1: - return self._get_images_bliss_1_11(scan, detector) - - if scan.scan_info["save"]: - lima_data = scan.streams[detector.image] - return lima_data[:] - elif "flat" in scan.name or "dark" in scan.name: - lima_data = list(image_from_server(detector.proxy, -1).array) - return lima_data - else: - chain_presets = next(scan.acq_chain._presets_master_list.values()) - for preset in chain_presets: - if isinstance(preset, ImageNoSavingPreset): - lima_data = preset._get_images() - return lima_data - # Detector focus alignment # @@ -263,7 +211,7 @@ class Align: ): if focus_scan is not None: # read back the aquired images - foc_images = self._get_images(focus_scan, detector) + foc_images = read_images(focus_scan, detector) # convert to numpy array foc_array = np.array(foc_images) @@ -391,7 +339,7 @@ class Align: projection=self.sequence._inpars.projection, save=save, ) - flat_images = self._get_images(flat_scan, detector) + flat_images = read_images(flat_scan, detector) flat_image = flat_images[-1] # take dark image @@ -404,7 +352,7 @@ class Align: save=save, ) # read back the aquired images - dark_images = self._get_images(dark_scan, detector) + dark_images = read_images(dark_scan, detector) dark_image = dark_images[-1] # pixel size in mm @@ -418,7 +366,7 @@ class Align: ) # read back the aquired images - al_images = self._get_images(al_scan, detector) + al_images = read_images(al_scan, detector) # works only with on motor today! lateral_motor = tomo_config.y_axis @@ -720,7 +668,7 @@ class Align: save=save, ) # read back the aquired images - alxc_images = self._get_images(alxc_scan, self.sequence._detector) + alxc_images = read_images(alxc_scan, self.sequence._detector) # print(alxc_images) # read back the motor positions @@ -737,7 +685,7 @@ class Align: save=save, ) # read back the aquired images - dark_images = self._get_images(dark_scan, self.sequence._detector) + dark_images = read_images(dark_scan, self.sequence._detector) dark_image = dark_images[0] distanceMove = abs(stop - start) @@ -834,30 +782,3 @@ class Align: DEFAULT_CHAIN.remove_preset(savingpreset) return None - - -class ImageNoSavingPreset(ChainPreset): - def __init__(self, detector): - self._images = [] - self._detector = detector - - class Iterator(ChainIterationPreset): - def __init__(self, images, detector): - self._images = images - self._detector = detector - - def prepare(self): - pass - - def start(self): - pass - - def stop(self): - self._images.append(image_from_server(self._detector.proxy, -1).array) - - def get_iterator(self, acq_chain): - while True: - yield ImageNoSavingPreset.Iterator(self._images, self._detector) - - def _get_images(self): - return self._images diff --git a/tomo/chain_presets/image_no_saving.py b/tomo/chain_presets/image_no_saving.py new file mode 100644 index 00000000..38089ef6 --- /dev/null +++ b/tomo/chain_presets/image_no_saving.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +import typing +import numpy + +try: + # BLISS >= 2.0 + bliss = 2 + from blissdata.lima.image_utils import image_from_server +except ImportError: + # BLISS <= 1.11 + bliss = 1 + from blissdata.data.lima_image import image_from_server + + +from bliss.scanning.chain import ChainPreset, ChainIterationPreset + +if typing.TYPE_CHECKING: + from bliss.controllers.lima.lima_base import Lima + + +class ImageNoSavingPreset(ChainPreset): + """ + Preset which fetch the detector data at the end of the scan. + + This can be used when a scan is not saved, and then the data is not sent + to the nexuswriter. + + Use :meth:`get_images` to access to the images from this preset at the + end of the scan. + """ + + def __init__(self, detector: Lima): + self._images: list[numpy.ndarray] = [] + self._detector = detector + + class Iterator(ChainIterationPreset): + def __init__(self, images, detector): + self._images = images + self._detector = detector + + def prepare(self): + pass + + def start(self): + pass + + def stop(self): + self._images.append(image_from_server(self._detector.proxy, -1).array) + + def get_iterator(self, acq_chain): + while True: + yield ImageNoSavingPreset.Iterator(self._images, self._detector) + + def _get_images(self) -> list[numpy.ndarray]: + """Deprecated, prefer to use the public API""" + return self.get_images() + + def get_images(self) -> list[numpy.ndarray]: + return self._images diff --git a/tomo/helpers/read_images.py b/tomo/helpers/read_images.py new file mode 100644 index 00000000..3a28d004 --- /dev/null +++ b/tomo/helpers/read_images.py @@ -0,0 +1,73 @@ +""" +Helper to read data. + +""" +from __future__ import annotations + +import numpy +import typing + +try: + # BLISS >= 2.0 + bliss = 2 + from blissdata.lima.image_utils import image_from_server +except ImportError: + # BLISS <= 1.11 + bliss = 1 + from blissdata.data.lima_image import image_from_server + +from tomo.chain_presets.image_no_saving import ImageNoSavingPreset + +if typing.TYPE_CHECKING: + from bliss.controllers.lima.lima_base import Lima + from bliss.scanning.scan import Scan + + +def _read_images_bliss_1_11(scan: Scan, detector: Lima) -> list[numpy.ndarray]: + """Compatibility with BLISS 1.11""" + from blissdata.data.events import ImageNotSaved + + scan_images = [] + lima_data = scan.get_data()[detector.image] + npoints = lima_data.last_index + + if npoints >= lima_data.buffer_max_number: + raise ValueError( + "Cannot read images! Not enough images in the Lima image buffer" + ) + + try: + for i in range(npoints): + scan_images.append(lima_data.get_image(i)) + + return scan_images + except ImageNotSaved: + chain_presets = next(scan.acq_chain._presets_master_list.values()) + for preset in chain_presets: + if isinstance(preset, ImageNoSavingPreset): + scan_images = preset.get_images() + return scan_images + + +def read_images(scan: Scan, detector: Lima) -> list[numpy.ndarray]: + """Fetch detector image from a scan. + + If the scan was not saved, the helper can fetch the images + from a `ImageNoSavingPreset` if the chain was setup with. + """ + if bliss == 1: + return _read_images_bliss_1_11(scan, detector) + + if scan.scan_info["save"]: + lima_data = scan.streams[detector.image] + return lima_data[:] + + if "flat" in scan.name or "dark" in scan.name: + return list(image_from_server(detector.proxy, -1).array) + + chain_presets = next(scan.acq_chain._presets_master_list.values()) + for preset in chain_presets: + if isinstance(preset, ImageNoSavingPreset): + return preset.get_images() + + raise RuntimeError("Unexpected scan and detector. No data to read.") -- GitLab From 5728483cb4cad482627fdaae1dacbb49b4419680 Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Tue, 19 Mar 2024 18:53:58 +0100 Subject: [PATCH 08/21] Context for image_no_saving --- tomo/align.py | 209 ++++++++++++++++++++++++-------------------------- 1 file changed, 101 insertions(+), 108 deletions(-) diff --git a/tomo/align.py b/tomo/align.py index 7bcf8312..a49cafea 100644 --- a/tomo/align.py +++ b/tomo/align.py @@ -5,6 +5,7 @@ import types import numpy as np import gevent import logging +import contextlib from matplotlib import pyplot as plt from bliss.common.scans import ascan, pointscan, DEFAULT_CHAIN @@ -24,6 +25,7 @@ from tomo.fulltomo import FullFieldTomo from tomo.controllers.tomo_config import TomoConfig from tomo.chain_presets.image_no_saving import ImageNoSavingPreset from tomo.helpers.read_images import read_images +from bliss.scanning.toolbox import DefaultAcquisitionChain if typing.TYPE_CHECKING: from bliss.common.axis import Axis @@ -42,6 +44,25 @@ except ImportError: nabu = None +@contextlib.contextmanager +def use_image_no_saving( + chain_builder: DefaultAcquisitionChain, + detector: Lima, + enabled: bool = False, +): + """Context to use a ImageNoSavingPreset to a chain builder""" + try: + if enabled: + preset = ImageNoSavingPreset(detector) + chain_builder.add_preset(preset) + else: + preset = None + yield preset + finally: + if enabled: + chain_builder.remove_preset(preset) + + class Align: """ Procedure to align the rotation stage. @@ -268,47 +289,37 @@ class Align: std = detector.roi_counters.counters.focus_roi_std counters = (std, detector.image) - if not save: - savingpreset = ImageNoSavingPreset(detector) - DEFAULT_CHAIN.add_preset(savingpreset) - # clean-up: move back focus motor, delete roi counters, move out paper - restore_list = (cleanup_axis.POS,) - with cleanup(*[focus_motor], restore_list=restore_list): - with capture_exceptions(raise_index=0) as capture: - with capture(): - # execute focus scan - scan = ascan( - focus_motor, - start, - stop, - steps, - expo_time, - counters, - title="focus scan", - save=save, - ) - - # get the position on the maximum of the standard deviation - # from bliss.scanning.scan_tools import peak - # pos_std_max = peak() - - # delete roi counters - del det_rois["focus_roi"] - - if not save: - DEFAULT_CHAIN.remove_preset(savingpreset) - - return scan - - # test if an error has occured - if len(capture.failed) > 0: - # delete roi counters - del det_rois["focus_roi"] - - if not save: - DEFAULT_CHAIN.remove_preset(savingpreset) - - return None + with use_image_no_saving(DEFAULT_CHAIN, detector, enabled=not save): + # clean-up: move back focus motor, delete roi counters, move out paper + restore_list = (cleanup_axis.POS,) + with cleanup(*[focus_motor], restore_list=restore_list): + with capture_exceptions(raise_index=0) as capture: + with capture(): + # execute focus scan + scan = ascan( + focus_motor, + start, + stop, + steps, + expo_time, + counters, + title="focus scan", + save=save, + ) + + # get the position on the maximum of the standard deviation + # from bliss.scanning.scan_tools import peak + # pos_std_max = peak() + + # delete roi counters + del det_rois["focus_roi"] + return scan + + # test if an error has occured + if len(capture.failed) > 0: + # delete roi counters + del det_rois["focus_roi"] + return None # Lateral alignment and camera tilt @@ -612,36 +623,27 @@ class Align: def align_scan( self, rotation_motor: Axis, expo_time: float, detector: Lima, save: bool = False ): - if not save: - savingpreset = ImageNoSavingPreset(detector) - DEFAULT_CHAIN.add_preset(savingpreset) - # clean-up: move back rot - restore_list = (cleanup_axis.POS,) - with cleanup(rotation_motor, restore_list=restore_list): - with capture_exceptions(raise_index=0) as capture: - with capture(): - # scan rot - rot_pos_list = [0.0, 180.0] - scan = pointscan( - rotation_motor, - rot_pos_list, - expo_time, - detector.image, - title="align scan", - save=save, - ) - - if not save: - DEFAULT_CHAIN.remove_preset(savingpreset) - - return scan - - # test if an error has occured - if len(capture.failed) > 0: - if not save: - DEFAULT_CHAIN.remove_preset(savingpreset) - - return None + with use_image_no_saving(DEFAULT_CHAIN, detector, enabled=not save): + # clean-up: move back rot + restore_list = (cleanup_axis.POS,) + with cleanup(rotation_motor, restore_list=restore_list): + with capture_exceptions(raise_index=0) as capture: + with capture(): + # scan rot + rot_pos_list = [0.0, 180.0] + scan = pointscan( + rotation_motor, + rot_pos_list, + expo_time, + detector.image, + title="align scan", + save=save, + ) + return scan + + # test if an error has occured + if len(capture.failed) > 0: + return None # Camera stage tilt alignment @@ -745,40 +747,31 @@ class Align: print(f"Current slit gap positions: vertical={vg_pos}, horizontal={hg_pos}") print(f"Current xc positions: xc={xc.position}") - if not save: - savingpreset = ImageNoSavingPreset(detector) - DEFAULT_CHAIN.add_preset(savingpreset) - # clean-up: move back xc motor, open_slits - restore_list = (cleanup_axis.POS,) - with error_cleanup(xc, svg, shg, restore_list=restore_list): - with capture_exceptions(raise_index=0) as capture: - with capture(): - # close the slits - umv(svg, slit_gap, shg, slit_gap) - - # scan xc - scan = ascan( - xc, - start, - stop, - steps, - expo_time, - detector.image, - title="align xc scan", - save=save, - ) - - # open the slits - umv(svg, vg_pos, shg, hg_pos) - - if not save: - DEFAULT_CHAIN.remove_preset(savingpreset) - # return images - return scan - - # test if an error has occured - if len(capture.failed) > 0: - if not save: - DEFAULT_CHAIN.remove_preset(savingpreset) - - return None + with use_image_no_saving(DEFAULT_CHAIN, detector, enabled=not save): + # clean-up: move back xc motor, open_slits + restore_list = (cleanup_axis.POS,) + with error_cleanup(xc, svg, shg, restore_list=restore_list): + with capture_exceptions(raise_index=0) as capture: + with capture(): + # close the slits + umv(svg, slit_gap, shg, slit_gap) + + # scan xc + scan = ascan( + xc, + start, + stop, + steps, + expo_time, + detector.image, + title="align xc scan", + save=save, + ) + + # open the slits + umv(svg, vg_pos, shg, hg_pos) + return scan + + # test if an error has occured + if len(capture.failed) > 0: + return None -- GitLab From 627d9ffa35130bd9101856e7c2757b03d34bd08d Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Tue, 19 Mar 2024 18:56:05 +0100 Subject: [PATCH 09/21] Try-finally to clean up focus roi --- tomo/align.py | 61 +++++++++++++++++++++++++-------------------------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/tomo/align.py b/tomo/align.py index a49cafea..0dc51043 100644 --- a/tomo/align.py +++ b/tomo/align.py @@ -289,37 +289,36 @@ class Align: std = detector.roi_counters.counters.focus_roi_std counters = (std, detector.image) - with use_image_no_saving(DEFAULT_CHAIN, detector, enabled=not save): - # clean-up: move back focus motor, delete roi counters, move out paper - restore_list = (cleanup_axis.POS,) - with cleanup(*[focus_motor], restore_list=restore_list): - with capture_exceptions(raise_index=0) as capture: - with capture(): - # execute focus scan - scan = ascan( - focus_motor, - start, - stop, - steps, - expo_time, - counters, - title="focus scan", - save=save, - ) - - # get the position on the maximum of the standard deviation - # from bliss.scanning.scan_tools import peak - # pos_std_max = peak() - - # delete roi counters - del det_rois["focus_roi"] - return scan - - # test if an error has occured - if len(capture.failed) > 0: - # delete roi counters - del det_rois["focus_roi"] - return None + try: + with use_image_no_saving(DEFAULT_CHAIN, detector, enabled=not save): + # clean-up: move back focus motor, delete roi counters, move out paper + restore_list = (cleanup_axis.POS,) + with cleanup(*[focus_motor], restore_list=restore_list): + with capture_exceptions(raise_index=0) as capture: + with capture(): + # execute focus scan + scan = ascan( + focus_motor, + start, + stop, + steps, + expo_time, + counters, + title="focus scan", + save=save, + ) + + # get the position on the maximum of the standard deviation + # from bliss.scanning.scan_tools import peak + # pos_std_max = peak() + return scan + + # test if an error has occured + if len(capture.failed) > 0: + return None + finally: + # delete roi counters + del det_rois["focus_roi"] # Lateral alignment and camera tilt -- GitLab From 83f36307f42c8f200d0108599acf95b6c70c209f Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Wed, 20 Mar 2024 16:39:31 +0100 Subject: [PATCH 10/21] image_no_saving: Robustness on simulator --- tomo/chain_presets/image_no_saving.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tomo/chain_presets/image_no_saving.py b/tomo/chain_presets/image_no_saving.py index 38089ef6..604834cd 100644 --- a/tomo/chain_presets/image_no_saving.py +++ b/tomo/chain_presets/image_no_saving.py @@ -2,6 +2,7 @@ from __future__ import annotations import typing import numpy +import time try: # BLISS >= 2.0 @@ -46,7 +47,18 @@ class ImageNoSavingPreset(ChainPreset): pass def stop(self): - self._images.append(image_from_server(self._detector.proxy, -1).array) + nb_retry = 4 + for i in range(nb_retry + 1): + try: + image = image_from_server(self._detector.proxy, -1).array + break + except IndexError: + # Image not yet ready + # Try a couple of times + if i >= nb_retry: + raise + time.sleep(0.05) + self._images.append(image) def get_iterator(self, acq_chain): while True: -- GitLab From 8c86eaec956eb1c4e93ae080c36378733ad35845 Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Wed, 20 Mar 2024 16:39:57 +0100 Subject: [PATCH 11/21] align: Protect projection --- tomo/align.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tomo/align.py b/tomo/align.py index 0dc51043..4c67df81 100644 --- a/tomo/align.py +++ b/tomo/align.py @@ -340,13 +340,19 @@ class Align: umv(rotation_axis, 0) # take reference image + + try: + projection = self.sequence._inpars.projection + except AttributeError: + projection = 1 + flat_n = 1 flat_runner = tomo_config.get_runner("flat") flat_scan = flat_runner( exposure_time, flat_n, detector, - projection=self.sequence._inpars.projection, + projection=projection, save=save, ) flat_images = read_images(flat_scan, detector) -- GitLab From 6e083c2190aa1425cee65c261f8a96593a19fdb8 Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Wed, 20 Mar 2024 16:40:24 +0100 Subject: [PATCH 12/21] align: Expose tomo config --- tomo/align.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tomo/align.py b/tomo/align.py index 4c67df81..f4cbd868 100644 --- a/tomo/align.py +++ b/tomo/align.py @@ -113,6 +113,10 @@ class Align: """ return self.__config + @property + def tomo_config(self) -> TomoConfig: + return self._tomo_config + def __info__(self) -> str: info_str = f"{self.name} info:\n" if self.sequence is not None: -- GitLab From c497d96353935a2e9021c772ecd30256b74eefaa Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Wed, 20 Mar 2024 17:22:25 +0100 Subject: [PATCH 13/21] align: tomo detector is not supposed to be None --- tomo/align.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tomo/align.py b/tomo/align.py index f4cbd868..4ff72273 100644 --- a/tomo/align.py +++ b/tomo/align.py @@ -393,6 +393,7 @@ class Align: rotc = optic.rotc_motor tomo_detector = tomo_config.detectors.get_tomo_detector(detector) + assert tomo_detector is not None source_sample_distance = tomo_config.source_sample_distance sample_detector_distance = tomo_detector.sample_detector_distance -- GitLab From 01516cc94a32edd5f9e70fb28defebca2e11468c Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Wed, 20 Mar 2024 14:45:11 +0100 Subject: [PATCH 14/21] align: test initialization --- tests/test_align.py | 21 +++++++++++++++++++++ tests/test_configuration/axes.yml | 14 ++++++++++++++ tests/test_configuration/test_align.yml | 8 ++++++++ 3 files changed, 43 insertions(+) create mode 100644 tests/test_align.py create mode 100644 tests/test_configuration/test_align.yml diff --git a/tests/test_align.py b/tests/test_align.py new file mode 100644 index 00000000..7e2490f0 --- /dev/null +++ b/tests/test_align.py @@ -0,0 +1,21 @@ +from bliss.common.measurementgroup import get_active as get_active_mg + + +def test_align(beacon, session, lima_simulator, mocker): + align = beacon.get("test_align") + assert align is not None + + tomo_config = align.tomo_config + active_detector = tomo_config.detectors.detectors[0] + tomo_config.active_detector = active_detector + align.sequence._detector = active_detector.detector + + mocker.patch.object(align, "align_calc", return_value=(1.0, 2.0)) + + # Add a counter to allow to `ct` from align to work + sim_ct_1 = beacon.get("sim_ct_1") + mg = beacon.get("mg") + mg.set_active() + mg.add(sim_ct_1) + + align.align(questions_move=False) diff --git a/tests/test_configuration/axes.yml b/tests/test_configuration/axes.yml index ca9d9f02..d546b426 100644 --- a/tests/test_configuration/axes.yml +++ b/tests/test_configuration/axes.yml @@ -15,3 +15,17 @@ low_limit: -10000 high_limit: 10000 unit: mm + - name: vg + steps_per_unit: 10000 + velocity: 20 + acceleration: 10000 + low_limit: -10000 + high_limit: 10000 + unit: mm + - name: hg + steps_per_unit: 10000 + velocity: 20 + acceleration: 10000 + low_limit: -10000 + high_limit: 10000 + unit: mm diff --git a/tests/test_configuration/test_align.yml b/tests/test_configuration/test_align.yml new file mode 100644 index 00000000..4d89290a --- /dev/null +++ b/tests/test_configuration/test_align.yml @@ -0,0 +1,8 @@ +- name: test_align + plugin: bliss + class: Align + package: tomo.optic.align + sequence: $fasttomo + paper: null + slits_vgap: $vg + slits_hgap: $hg -- GitLab From e5f3d5b029e756a1d3da9070472db12eefc8a340 Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Thu, 21 Mar 2024 14:38:08 +0100 Subject: [PATCH 15/21] Helper to get_active_tomo_config --- tomo/globals.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tomo/globals.py b/tomo/globals.py index c82b5f75..32759f24 100644 --- a/tomo/globals.py +++ b/tomo/globals.py @@ -1,3 +1,4 @@ +from __future__ import annotations from .helpers.active_object import ActiveObject as _ActiveObject from .tomoconfig import TomoConfig as _TomoConfig @@ -11,3 +12,10 @@ ACTIVE_TOMOCONFIG = _ActiveObject( USE_METADATA_FOR_SCAN = False """If true, the virtual controllers will stores themself the metadata""" + + +def get_active_tomo_config() -> _TomoConfig: + config = ACTIVE_TOMOCONFIG.deref_active_object() + if config is None: + raise RuntimeError("No active tomo config setup yet") + return config -- GitLab From e04b664ae8ffc2cefaa9f3ec49f112af79d6baef Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Thu, 21 Mar 2024 14:38:23 +0100 Subject: [PATCH 16/21] Black --- tomo/helpers/active_object.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tomo/helpers/active_object.py b/tomo/helpers/active_object.py index cd5d6353..b9943ec0 100644 --- a/tomo/helpers/active_object.py +++ b/tomo/helpers/active_object.py @@ -3,6 +3,7 @@ import functools from bliss.common.proxy import Proxy from bliss import current_session from bliss.config import settings + try: # BLISS >= 2.0 from bliss.config.conductor.client import get_redis_proxy -- GitLab From a86df3f11223715b80be5e26331811f4dd87a30a Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Thu, 21 Mar 2024 14:38:49 +0100 Subject: [PATCH 17/21] procedure: Create bases for procedures --- tomo/procedures/__init__.py | 0 tomo/procedures/base_procedure.py | 274 ++++++++++++++++++++++++++++++ 2 files changed, 274 insertions(+) create mode 100644 tomo/procedures/__init__.py create mode 100644 tomo/procedures/base_procedure.py diff --git a/tomo/procedures/__init__.py b/tomo/procedures/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tomo/procedures/base_procedure.py b/tomo/procedures/base_procedure.py new file mode 100644 index 00000000..b50d539b --- /dev/null +++ b/tomo/procedures/base_procedure.py @@ -0,0 +1,274 @@ +from __future__ import annotations +import abc +import enum +import gevent +import tblib +import traceback +import logging +import typing +from bliss.scanning.scan import Scan +from bliss.config.beacon_object import BeaconObject, EnumProperty + +_logger = logging.getLogger(__name__) + +try: + import mmh3 +except ImportError: + # For now, will BLISS 2.0 is not used + _logger.error("mmh3 is not installed") + mmh3 = None + + +class ProcedureExecusionState(enum.Enum): + NONE = "NONE" + """No run yet""" + + ABORTED = "ABORTED" + """Run was aborted""" + + FAILED = "FAILED" + """Run have failed""" + + SUCCESSED = "SUCCESSED" + """Run have successed""" + + +class ProcedureState(enum.Enum): + STANDBY = "STANDBY" + """Session scope procedure doing nothing""" + + DISABLED = "DISABLED" + """Session scope procedure which can't be actually started""" + + RUNNING = "RUNNING" + """Procedure doing something""" + + ABORTING = "ABORTING" + """Procedure about to be aborted""" + + WAITING_USER_INPUT = "WAITING_USER_INPUT" + """Temporarly interrupted running procedure until a user input is send""" + + UNKNOWN = "UNKNOWN" + """The procedure have an undefined state. + + This only exists to share unexpected behavior with the sub systems. + This indicates an implementation or environement problem. + This should not be implementors directly. + """ + + +class BaseProcedure(abc.ABC): + @property + @abc.abstractmethod + def state(self) -> ProcedureState: + ... + + +class SessionProcedure(BaseProcedure, BeaconObject): + """Procedure which is available durring the whole BLISS session. + + It can be instanciated as a BLISS object in the yaml file. + """ + + def __init__(self, name: str, config: dict): + BaseProcedure.__init__(self) + BeaconObject.__init__(self, config) + + self._starting: bool = False + self._greenlet: gevent.Greenlet | None = None + self._aborting: bool = False + self._request_user_input: bool = False + self._is_done = gevent.event.Event() + self._is_validated = gevent.event.Event() + if self._state == ProcedureState.UNKNOWN: + self._state = ProcedureState.STANDBY + + name = BeaconObject.config_getter("name") + + _state = EnumProperty( + "state", + default=ProcedureState.STANDBY, + unknown_value=ProcedureState.UNKNOWN, + enum_type=ProcedureState, + ) + + previous_run_state = EnumProperty( + "previous_run_state", + default=ProcedureExecusionState.NONE, + unknown_value=ProcedureExecusionState.NONE, + enum_type=ProcedureExecusionState, + ) + + previous_run_exception = BeaconObject.property_setting( + "previous_run_exception", None + ) + + previous_run_traceback = BeaconObject.property_setting( + "previous_run_traceback", None + ) + + parameters = BeaconObject.property_setting("parameters", {}) + """Parameters are content evolving duing the prodecude. + + It have to stay light and serializable. + For example, better to use reference that real data, + """ + + def update_parameters(self, **kwargs: dict[str, typing.Any]): + """Allow to update the parameters with automatic references. + + Scans are replaced by references. This can be extanded with + other BLISS concepts. + """ + values = dict(self.parameters) + for k, v in kwargs.items(): + if isinstance(v, Scan): + scan = v + node = scan.scan_info["node_name"] + value = {"__type__": "scan", "node": node} + if mmh3 is not None: + # Simplification for Daiquiri + BLISS < 2.0 + value["mmh3"] = mmh3.hash(node) & 0xFFFFFFFF + else: + value = v + values[k] = value + self.parameters = values + + @property + def state(self): + """Returns the actual state of the device""" + return self._state + + def _get_state(self): + if self._aborting: + return ProcedureState.ABORTING + if self._request_user_input: + return ProcedureState.WAITING_USER_INPUT + if self._starting: + return ProcedureState.RUNNING + if self._greenlet is not None: + return ProcedureState.RUNNING + return ProcedureState.STANDBY + + def _update_state(self): + s = self._get_state() + if s != self._state: + self._state = s + + @abc.abstractmethod + def _run(self): + ... + + def _done(self, greenlet: gevent.Greenlet): + self._aborting = False + self._greenlet = None + + # Get the state of the execusion + try: + result = greenlet.get() + except KeyboardInterrupt: + run_state = ProcedureExecusionState.ABORTED + except BaseException as e: + _logger.error("Error while running {self.name}", exc_info=True) + run_state = ProcedureExecusionState.FAILED + self.previous_run_exception = str(e) + traceback_dict = tblib.Traceback(e.__traceback__).to_dict() + self.previous_run_traceback = traceback_dict + else: + if isinstance(result, gevent.GreenletExit): + run_state = ProcedureExecusionState.ABORTED + else: + run_state = ProcedureExecusionState.SUCCESSED + if self.previous_run_state != run_state: + self.previous_run_state = run_state + + self._update_state() + self._is_done.set() + + def request_user_input(self): + self._request_user_input = True + + def clear(self): + """Clear information from the previous run""" + self._request_user_input = False + self.previous_run_state = ProcedureExecusionState.NONE + self.previous_run_exception = None + self.previous_run_traceback = None + self.parameters = {} + + def run(self, wait=True): + """Run the procedure. + + This is like a scan, it is blocking except `wait` is set to `False`. + """ + self.start() + if wait: + self.wait() + + def start(self): + """Start the procedure asynchronously.""" + if self._greenlet is not None: + raise RuntimeError("Procedure already running") + self.clear() + try: + self._starting = True + self._is_done.clear() + self._update_state() + self._greenlet = gevent.spawn(self._run) + self._greenlet.link(self._done) + finally: + self._starting = False + self._update_state() + + def request_and_wait_validation(self): + self._is_validated.clear() + try: + self._request_user_input = True + self._update_state() + self._is_validated.wait() + finally: + self._request_user_input = False + self._update_state() + + def validate(self): + """Validate a pending user validation""" + self._is_validated.set() + + def abort(self): + state = self._state + if self._greenlet is None: + if state in [ProcedureState.RUNNING, ProcedureState.WAITING_USER_INPUT]: + # Let's assume the state was not porperly reset + self._update_state() + return + raise RuntimeError("Procedure not running") + self._aborting = True + self._update_state() + self._greenlet.kill(block=False) + + def wait(self): + self._is_done.wait() + + def __info__(self) -> str: + lines: list[str] = [] + lines += (f"name: {self.name}",) + lines += (f"class: {type(self).__module__}.{type(self).__name__}",) + lines += (f"state: {self.state.name}",) + if self.previous_run_state != ProcedureExecusionState.NONE: + lines += ("previous run:",) + lines += (f" state: {self.previous_run_state.name}",) + if self.previous_run_exception is not None: + message = str(self.previous_run_exception) + message = message.replace("\n", "\n | ") + lines += (" exception:",) + lines += (f" | {message}",) + if self.previous_run_traceback is not None: + lines += (" traceback:",) + tb = tblib.Traceback.from_dict(self.previous_run_traceback) + trace = traceback.format_tb(tb) + for t in trace: + t = t.rstrip("\n") + t = t.replace("\n", "\n | ") + lines.append(f" | {t}") + return "\n".join(lines) -- GitLab From 07e3457a95fd973b06021ec0fceed0dd23b44990 Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Thu, 21 Mar 2024 14:39:10 +0100 Subject: [PATCH 18/21] Create align procedure --- tomo/procedures/align.py | 249 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 tomo/procedures/align.py diff --git a/tomo/procedures/align.py b/tomo/procedures/align.py new file mode 100644 index 00000000..a5eb18e8 --- /dev/null +++ b/tomo/procedures/align.py @@ -0,0 +1,249 @@ +from __future__ import annotations +import typing +import logging +import numpy +from bliss.config import static +from .base_procedure import SessionProcedure +from tomo.globals import get_active_tomo_config +from bliss.shell.standard import umv, umvr, ct, pointscan +from tomo.helpers.read_images import read_images +from bliss.shell.getval import getval_yes_no +from bliss.common.cleanup import ( + cleanup, + capture_exceptions, + axis as cleanup_axis, +) +from tomo.helpers.shell_utils import is_in_shell + +if typing.TYPE_CHECKING: + from tomo.controllers.lima.lima_base import Lima + from tomo.common.axis import Axis + +_logger = logging.getLogger(__name__) + +try: + import nabu + from nabu.estimation.tilt import CameraTilt +except ImportError: + _logger.error("Error while importing nabu", exc_info=True) + nabu = None + + +class Correction(typing.NamedTuple): + lat_motor_name: str + """Motor controlling the translation under the rotation""" + + method: str = "" + """Method used by nabu""" + + lat_corr: float | None = None + """Translation for the motor under the rotation, in mm""" + + camera_tilt_motor_name: str | None = None + """Motor controlling the detector tilt""" + + camera_tilt: float | None = None + """Rotation for the tilt of the detector, in deg""" + + +class Align(SessionProcedure): + def __init__(self, name: str, config: dict): + SessionProcedure.__init__(self, name, config) + self._use_fft_polar_method = False + + def _run(self): + tomo_config = get_active_tomo_config() + exposure_time = tomo_config.pars.exposure_time + tomo_detector = tomo_config.detectors.active_detector + if tomo_detector is None: + raise RuntimeError("No detector active yet") + + detector = tomo_detector.detector + optic = tomo_detector.optic + rotation_axis = tomo_config.rotation_axis + + # Setup the kind of correction + lateral_motor = tomo_config.y_axis + correction = Correction( + lat_motor_name=lateral_motor.name, + ) + + try: + rotc_motor = optic.rotc_motor + except Exception: + rotc_motor = None + print("No motor for the tilt correction") + if rotc_motor is not None: + correction = correction._replace( + camera_tilt_motor_name=rotc_motor.name, + ) + + # move rotation axis to 0 + umv(rotation_axis, 0) + + # take flat image + flat_n = 1 + flat_runner = tomo_config.get_runner("flat") + flat_scan = flat_runner( + exposure_time, + flat_n, + detector, + projection=1, + save=True, + ) + self.update_parameters(flat_scan=flat_scan) + flat_images = read_images(flat_scan, detector) + flat_image = flat_images[-1] + + # take dark image + dark_n = 1 + dark_runner = tomo_config.get_runner("dark") + dark_scan = dark_runner( + exposure_time, + dark_n, + detector, + save=True, + ) + # read back the aquired images + self.update_parameters(dark_scan=dark_scan) + dark_images = read_images(dark_scan, detector) + dark_image = dark_images[-1] + + # pixel size in mm + pixel_size = tomo_detector.sample_pixel_size / 1000.0 + + # do alignment scan + al_scan = self._align_scan( + rotation_axis, + exposure_time, + detector, + save=True, + ) + self.update_parameters(proj_scan=al_scan) + + # read back the aquired images + al_images = read_images(al_scan, detector) + + source_sample_distance = tomo_config.source_sample_distance + sample_detector_distance = tomo_detector.sample_detector_distance + + correction = self._guess_correction( + correction, + al_images, + dark_image, + flat_image, + pixel_size, + source_sample_distance, + sample_detector_distance, + ) + + self.update_parameters(**correction._asdict()) + self.update_parameters(in_shell=is_in_shell()) + if is_in_shell(): + correction = self._validate_correction_in_shell(correction) + else: + print("!!! Not running in BLISS shell !!!") + self.request_and_wait_validation() + parameters = self.parameters + corr = {} + for kk in Correction._fields: + k = kk[0] + if k in parameters: + corr[k] = parameters[k] + correction = correction._replace(**corr) + + self._apply_correction(correction) + + ct(exposure_time) + print(f"Lateral alignment motor position = {lateral_motor.position}") + if rotc_motor is not None: + print(f"Camera tilt position {rotc_motor.name} = {rotc_motor.position}") + + def _align_scan( + self, rotation_motor: Axis, expo_time: float, detector: Lima, save: bool = False + ): + # clean-up: move back rot + restore_list = (cleanup_axis.POS,) + with cleanup(rotation_motor, restore_list=restore_list): + with capture_exceptions(raise_index=0) as capture: + with capture(): + # scan rot + rot_pos_list = [0.0, 180.0] + scan = pointscan( + rotation_motor, + rot_pos_list, + expo_time, + detector.image, + title="align scan", + save=save, + ) + return scan + + # test if an error has occured + if len(capture.failed) > 0: + return None + + def _guess_correction( + self, + correction: Correction, + al_images, + dark_image, + flat_image, + pixel_size, + source_sample_distance, + sample_detector_distance, + ) -> Correction: + if nabu is None: + print("nabu is not installed") + return correction + dark = dark_image.astype(float) + flat = flat_image.astype(float) + radio0 = (al_images[0].astype(float) - dark) / (flat - dark) + radio180 = (al_images[1].astype(float) - dark) / (flat - dark) + + # flip the radioumvr(lateral_motor, correction.lat_cor180 for the calculation + radio180_flip = numpy.fliplr(radio180) + + # calculate the lateral correction for the rotation axis + # and the camera tilt with line by line correlation + tilt_calc = CameraTilt() + + method = "1d-correlation" + if self._use_fft_polar_method: + method = "fft-polar" + pixel_cor, camera_tilt = tilt_calc.compute_angle( + radio0, radio180_flip, method=method + ) + + lat_corr = -pixel_size * pixel_cor + return correction._replace( + method=method, lat_corr=lat_corr, camera_tilt=camera_tilt + ) + + def _validate_correction_in_shell( + self, + correction: Correction, + ) -> Correction: + """User validation to accept or reject the correction""" + if not getval_yes_no("Apply the lateral position correction?", default=False): + correction = correction._replace(lat_corr=None) + if not getval_yes_no("Apply the camera tilt?", default=False): + correction = correction._replace(camera_tilt=None) + return correction + + def _apply_correction(self, correction: Correction): + config = static.get_config() + if correction.camera_tilt_motor_name is not None: + rotc_motor = config.get(correction.camera_tilt_motor_name) + else: + rotc_motor = None + lateral_motor = config.get(correction.lat_motor_name) + + if correction.lat_corr is not None: + # apply lateral correction + umvr(lateral_motor, correction.lat_corr) + + if rotc_motor is not None and correction.camera_tilt is not None: + # apply tilt correction + umvr(rotc_motor, correction.camera_tilt) + print("Correction successfully applied") -- GitLab From 4468c6ac7c6b343f288e2ee1e71774228fb7182d Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Fri, 19 Apr 2024 21:17:00 +0200 Subject: [PATCH 19/21] Added use_image_no_saving --- tomo/chain_presets/image_no_saving.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tomo/chain_presets/image_no_saving.py b/tomo/chain_presets/image_no_saving.py index 604834cd..41371adf 100644 --- a/tomo/chain_presets/image_no_saving.py +++ b/tomo/chain_presets/image_no_saving.py @@ -3,6 +3,7 @@ from __future__ import annotations import typing import numpy import time +import contextlib try: # BLISS >= 2.0 @@ -20,6 +21,25 @@ if typing.TYPE_CHECKING: from bliss.controllers.lima.lima_base import Lima +@contextlib.contextmanager +def use_image_no_saving( + chain_builder: DefaultAcquisitionChain, + detector: Lima, + enabled: bool = False, +): + """Context to use a ImageNoSavingPreset to a chain builder""" + try: + if enabled: + preset = ImageNoSavingPreset(detector) + chain_builder.add_preset(preset) + else: + preset = None + yield preset + finally: + if enabled: + chain_builder.remove_preset(preset) + + class ImageNoSavingPreset(ChainPreset): """ Preset which fetch the detector data at the end of the scan. -- GitLab From 3b000c6a72e1bf8bde768f8392b243117cdb0e49 Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Fri, 19 Apr 2024 21:17:21 +0200 Subject: [PATCH 20/21] Clean up older align procedure --- tomo/align.py | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/tomo/align.py b/tomo/align.py index 4ff72273..8c01fd07 100644 --- a/tomo/align.py +++ b/tomo/align.py @@ -5,7 +5,6 @@ import types import numpy as np import gevent import logging -import contextlib from matplotlib import pyplot as plt from bliss.common.scans import ascan, pointscan, DEFAULT_CHAIN @@ -23,7 +22,7 @@ from bliss.shell.standard import umv, mv, umvr, flint from bliss.controllers.multiplepositions import MultiplePositions from tomo.fulltomo import FullFieldTomo from tomo.controllers.tomo_config import TomoConfig -from tomo.chain_presets.image_no_saving import ImageNoSavingPreset +from tomo.chain_presets.image_no_saving import use_image_no_saving from tomo.helpers.read_images import read_images from bliss.scanning.toolbox import DefaultAcquisitionChain @@ -44,25 +43,6 @@ except ImportError: nabu = None -@contextlib.contextmanager -def use_image_no_saving( - chain_builder: DefaultAcquisitionChain, - detector: Lima, - enabled: bool = False, -): - """Context to use a ImageNoSavingPreset to a chain builder""" - try: - if enabled: - preset = ImageNoSavingPreset(detector) - chain_builder.add_preset(preset) - else: - preset = None - yield preset - finally: - if enabled: - chain_builder.remove_preset(preset) - - class Align: """ Procedure to align the rotation stage. -- GitLab From e378816ee0661be47bc3785b5984468ac6052051 Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Fri, 19 Apr 2024 21:17:51 +0200 Subject: [PATCH 21/21] Update align procedure to be used with daiquiri --- tomo/procedures/align.py | 139 +++++++++++++++++++++++++++------------ 1 file changed, 97 insertions(+), 42 deletions(-) diff --git a/tomo/procedures/align.py b/tomo/procedures/align.py index a5eb18e8..1baea9b6 100644 --- a/tomo/procedures/align.py +++ b/tomo/procedures/align.py @@ -14,6 +14,11 @@ from bliss.common.cleanup import ( axis as cleanup_axis, ) from tomo.helpers.shell_utils import is_in_shell +from bliss.scanning.group import Sequence +from bliss.scanning.chain import AcquisitionChannel +from silx.math.combo import min_max +from bliss.common.scans import DEFAULT_CHAIN +from tomo.chain_presets.image_no_saving import use_image_no_saving if typing.TYPE_CHECKING: from tomo.controllers.lima.lima_base import Lima @@ -61,6 +66,7 @@ class Align(SessionProcedure): detector = tomo_detector.detector optic = tomo_detector.optic rotation_axis = tomo_config.rotation_axis + save = False # Setup the kind of correction lateral_motor = tomo_config.y_axis @@ -78,19 +84,24 @@ class Align(SessionProcedure): camera_tilt_motor_name=rotc_motor.name, ) + self.update_parameters( + detector_channel=f"{detector.name}:{detector.image.name}" + ) + # move rotation axis to 0 umv(rotation_axis, 0) # take flat image flat_n = 1 flat_runner = tomo_config.get_runner("flat") - flat_scan = flat_runner( - exposure_time, - flat_n, - detector, - projection=1, - save=True, - ) + with use_image_no_saving(DEFAULT_CHAIN, detector, enabled=not save): + flat_scan = flat_runner( + exposure_time, + flat_n, + detector, + projection=1, + save=save, + ) self.update_parameters(flat_scan=flat_scan) flat_images = read_images(flat_scan, detector) flat_image = flat_images[-1] @@ -98,12 +109,14 @@ class Align(SessionProcedure): # take dark image dark_n = 1 dark_runner = tomo_config.get_runner("dark") - dark_scan = dark_runner( - exposure_time, - dark_n, - detector, - save=True, - ) + with use_image_no_saving(DEFAULT_CHAIN, detector, enabled=not save): + dark_scan = dark_runner( + exposure_time, + dark_n, + detector, + save=save, + ) + # read back the aquired images self.update_parameters(dark_scan=dark_scan) dark_images = read_images(dark_scan, detector) @@ -117,27 +130,73 @@ class Align(SessionProcedure): rotation_axis, exposure_time, detector, - save=True, + save=save, ) self.update_parameters(proj_scan=al_scan) # read back the aquired images al_images = read_images(al_scan, detector) + def flatfield(proj: numpy.ndarray, dark: numpy.ndarray, flat: numpy.ndarray): + proj = proj.astype(numpy.float32) + dark = dark.astype(numpy.float32) + flat = flat.astype(numpy.float32) + with numpy.errstate(divide="ignore", invalid="ignore"): + proj = (proj - dark) / (flat - dark) + # Clean up dark bigger than proj + proj[proj < 0] = 0 + # Clean up division by zero + proj[numpy.isnan(proj)] = 0 + return proj + + proj0 = flatfield(al_images[0], dark_image, flat_image) + proj180 = flatfield(al_images[1], dark_image, flat_image) + proj0_state = min_max(proj0, min_positive=True, finite=True) + proj180_state = min_max(proj180, min_positive=True, finite=True) + + seq = Sequence() + shape = detector.image.fullsize + seq.add_custom_channel(AcquisitionChannel("proj0", numpy.float32, shape)) + seq.add_custom_channel(AcquisitionChannel("proj0_min", numpy.float32, tuple( ))) + seq.add_custom_channel(AcquisitionChannel("proj0_max", numpy.float32, tuple( ))) + seq.add_custom_channel(AcquisitionChannel("proj0_mean", numpy.float32, tuple( ))) + seq.add_custom_channel(AcquisitionChannel("proj0_std", numpy.float32, tuple( ))) + seq.add_custom_channel(AcquisitionChannel("proj0_min_positive", numpy.float32, tuple( ))) + seq.add_custom_channel(AcquisitionChannel("proj180", numpy.float32, shape)) + seq.add_custom_channel(AcquisitionChannel("proj180_min", numpy.float32, tuple( ))) + seq.add_custom_channel(AcquisitionChannel("proj180_max", numpy.float32, tuple( ))) + seq.add_custom_channel(AcquisitionChannel("proj180_mean", numpy.float32, tuple( ))) + seq.add_custom_channel(AcquisitionChannel("proj180_std", numpy.float32, tuple( ))) + seq.add_custom_channel(AcquisitionChannel("proj180_min_positive", numpy.float32, tuple( ))) + with seq.sequence_context(): + seq.custom_channels["proj0"].emit([proj0]) + seq.custom_channels["proj0_min"].emit([proj0_state.minimum]) + seq.custom_channels["proj0_max"].emit([proj0_state.maximum]) + seq.custom_channels["proj0_mean"].emit([numpy.nanmean(proj0)]) + seq.custom_channels["proj0_std"].emit([numpy.nanstd(proj0)]) + seq.custom_channels["proj0_min_positive"].emit([proj0_state.min_positive]) + seq.custom_channels["proj180"].emit([proj180]) + seq.custom_channels["proj180_min"].emit([proj180_state.minimum]) + seq.custom_channels["proj180_max"].emit([proj180_state.maximum]) + seq.custom_channels["proj180_mean"].emit([numpy.nanmean(proj180)]) + seq.custom_channels["proj180_std"].emit([numpy.nanstd(proj180)]) + seq.custom_channels["proj180_min_positive"].emit([proj180_state.min_positive]) + source_sample_distance = tomo_config.source_sample_distance sample_detector_distance = tomo_detector.sample_detector_distance + self.update_parameters(data_scan=seq.scan) correction = self._guess_correction( correction, - al_images, - dark_image, - flat_image, + proj0, + proj180, pixel_size, source_sample_distance, sample_detector_distance, ) self.update_parameters(**correction._asdict()) + self.update_parameters(pixel_size_mm=pixel_size) self.update_parameters(in_shell=is_in_shell()) if is_in_shell(): correction = self._validate_correction_in_shell(correction) @@ -164,31 +223,31 @@ class Align(SessionProcedure): ): # clean-up: move back rot restore_list = (cleanup_axis.POS,) - with cleanup(rotation_motor, restore_list=restore_list): - with capture_exceptions(raise_index=0) as capture: - with capture(): - # scan rot - rot_pos_list = [0.0, 180.0] - scan = pointscan( - rotation_motor, - rot_pos_list, - expo_time, - detector.image, - title="align scan", - save=save, - ) - return scan - - # test if an error has occured - if len(capture.failed) > 0: - return None + with use_image_no_saving(DEFAULT_CHAIN, detector, enabled=not save): + with cleanup(rotation_motor, restore_list=restore_list): + with capture_exceptions(raise_index=0) as capture: + with capture(): + # scan rot + rot_pos_list = [0.0, 180.0] + scan = pointscan( + rotation_motor, + rot_pos_list, + expo_time, + detector.image, + title="align scan", + save=save, + ) + return scan + + # test if an error has occured + if len(capture.failed) > 0: + return None def _guess_correction( self, correction: Correction, - al_images, - dark_image, - flat_image, + radio0, + radio180, pixel_size, source_sample_distance, sample_detector_distance, @@ -196,10 +255,6 @@ class Align(SessionProcedure): if nabu is None: print("nabu is not installed") return correction - dark = dark_image.astype(float) - flat = flat_image.astype(float) - radio0 = (al_images[0].astype(float) - dark) / (flat - dark) - radio180 = (al_images[1].astype(float) - dark) / (flat - dark) # flip the radioumvr(lateral_motor, correction.lat_cor180 for the calculation radio180_flip = numpy.fliplr(radio180) -- GitLab