diff --git a/csaxs_bec/bec_ipython_client/plugins/flomni/flomni.py b/csaxs_bec/bec_ipython_client/plugins/flomni/flomni.py index 82b4a96..9da179b 100644 --- a/csaxs_bec/bec_ipython_client/plugins/flomni/flomni.py +++ b/csaxs_bec/bec_ipython_client/plugins/flomni/flomni.py @@ -9,12 +9,13 @@ import numpy as np from bec_lib import bec_logger from bec_lib.alarm_handler import AlarmBase from bec_lib.pdf_writer import PDFWriter +from bec_lib.scan_repeat import scan_repeat from typeguard import typechecked from csaxs_bec.bec_ipython_client.plugins.cSAXS import cSAXSBeamlineChecks from csaxs_bec.bec_ipython_client.plugins.flomni.flomni_optics_mixin import FlomniOpticsMixin -from csaxs_bec.bec_ipython_client.plugins.flomni.x_ray_eye_align import XrayEyeAlign from csaxs_bec.bec_ipython_client.plugins.flomni.gui_tools import flomniGuiTools +from csaxs_bec.bec_ipython_client.plugins.flomni.x_ray_eye_align import XrayEyeAlign from csaxs_bec.bec_ipython_client.plugins.omny.omny_general_tools import ( OMNYTools, PtychoReconstructor, @@ -786,9 +787,7 @@ class FlomniSampleTransferMixin: dev.ftransy.controller.socket_put_confirmed("confirm=1") else: print("Stopping.") - raise FlomniError( - "User abort sample transfer." - ) + raise FlomniError("User abort sample transfer.") def ftransfer_gripper_is_open(self) -> bool: status = bool(float(dev.ftransy.controller.socket_put_and_receive("MG @OUT[9]").strip())) @@ -811,7 +810,7 @@ class FlomniSampleTransferMixin: def ftransfer_gripper_move(self, position: int): self.check_position_is_valid(position) - #this is not used for sample stage position! + # this is not used for sample stage position! self._ftransfer_shiftx = -0.15 self._ftransfer_shiftz = -0.5 @@ -1237,6 +1236,7 @@ class _ProgressProxy: "tomo_type": 0, "tomo_start_time": None, "estimated_remaining_time": None, + "estimated_finish_time": None, "heartbeat": None, } @@ -1314,12 +1314,13 @@ class Flomni( from csaxs_bec.bec_ipython_client.plugins.flomni.flomni_webpage_generator import ( FlomniWebpageGenerator, ) + self._webpage_gen = FlomniWebpageGenerator( bec_client=client, output_dir="~/data/raw/webpage/", - #upload_url="http://s1090968537.online.de/upload.php", # optional + # upload_url="http://s1090968537.online.de/upload.php", # optional upload_url="https://v1p0zyg2w9n2k9c1.myfritz.net/upload.php", - local_port=8080 + local_port=8080, ) self._webpage_gen.start() @@ -1578,6 +1579,22 @@ class Flomni( def tomo_stitch_overlap(self, val: float): self.client.set_global_var("tomo_stitch_overlap", val) + @property + def tomo_angle_range(self): + """Total angular sweep in degrees for tomo_type 1 (equally spaced + sub-tomograms), inclusive of the upper bound. Either 180 (default, + original behaviour) or 360.""" + val = self.client.get_global_var("tomo_angle_range") + if val is None: + return 180 + return val + + @tomo_angle_range.setter + def tomo_angle_range(self, val: float): + if val not in (180, 360): + raise ValueError("tomo_angle_range must be 180 or 360 degrees.") + self.client.set_global_var("tomo_angle_range", val) + @property def golden_projections_at_0_deg_for_damage_estimation(self): val = self.client.get_global_var("golden_projections_at_0_deg_for_damage_estimation") @@ -1600,6 +1617,39 @@ class Flomni( def golden_ratio_bunch_size(self, val: float): self.client.set_global_var("golden_ratio_bunch_size", val) + @property + def frames_per_trigger(self): + """Number of burst frames acquired per point/projection. Used both + by scans.flomni_fermat_scan (via tomo_scan_projection) and by + tomo_acquire_at_angle (scans.acquire).""" + val = self.client.get_global_var("frames_per_trigger") + if val is None: + return 1 + return val + + @frames_per_trigger.setter + def frames_per_trigger(self, val: int): + if isinstance(val, bool) or not isinstance(val, int): + raise ValueError("frames_per_trigger must be a positive integer.") + if val <= 0: + raise ValueError("frames_per_trigger must be a positive integer.") + self.client.set_global_var("frames_per_trigger", val) + + @property + def single_point_instead_of_fermat_scan(self): + """If True, tomo_scan acquires a single point (or burst) at each + angle via scans.acquire instead of running scans.flomni_fermat_scan. + Applies to all tomo_types, since it only changes how a given angle + is acquired, not which angles are visited.""" + val = self.client.get_global_var("single_point_instead_of_fermat_scan") + if val is None: + return False + return val + + @single_point_instead_of_fermat_scan.setter + def single_point_instead_of_fermat_scan(self, val: bool): + self.client.set_global_var("single_point_instead_of_fermat_scan", val) + @property def sample_name(self): return self.sample_get_name(0) @@ -1645,7 +1695,7 @@ class Flomni( end_scan_number = bec.queue.next_scan_number for scan_nr in range(start_scan_number, end_scan_number): - #self._write_tomo_scan_number(scan_nr, angle, 0) + # self._write_tomo_scan_number(scan_nr, angle, 0) alignment_scan_numbers.append(scan_nr) umv(dev.fsamroy, 0) @@ -1655,7 +1705,7 @@ class Flomni( # summary of alignment scan numbers scan_list_str = ", ".join(str(s) for s in alignment_scan_numbers) - #print(f"\nAlignment scan numbers ({len(alignment_scan_numbers)} total): {scan_list_str}") + # print(f"\nAlignment scan numbers ({len(alignment_scan_numbers)} total): {scan_list_str}") # BEC scilog entry (no logo) scilog_content = ( @@ -1665,7 +1715,9 @@ class Flomni( f"Alignment scan numbers: {scan_list_str}\n" ) print(scilog_content) - bec.messaging.scilog.new().add_text(scilog_content.replace("\n", "
")).add_tags("alignmentscan").send() + bec.messaging.scilog.new().add_text(scilog_content.replace("\n", "
")).add_tags( + "alignmentscan" + ).send() def write_alignment_scan_numbers(self, first_scan): import os @@ -1691,8 +1743,6 @@ class Flomni( f.write(" ".join(map(str, x_vals)) + "\n") f.write(" ".join(map(str, zeros)) + "\n") - - def sub_tomo_scan(self, subtomo_number, start_angle=None): """ Performs a sub tomogram scan. @@ -1729,14 +1779,14 @@ class Flomni( start = start_angle + _tomo_shift_angles if subtomo_number % 2: # odd = forward - max_allowed_angle = 180.05 + self.tomo_angle_stepsize - proposed_end = start + 180 + max_allowed_angle = self.tomo_angle_range + 0.05 + self.tomo_angle_stepsize + proposed_end = start + self.tomo_angle_range angle_end = min(proposed_end, max_allowed_angle) span = angle_end - start else: # even = reverse min_allowed_angle = 0 - proposed_end = start - 180 + proposed_end = start - self.tomo_angle_range angle_end = max(proposed_end, min_allowed_angle) span = start - angle_end @@ -1747,8 +1797,8 @@ class Flomni( if subtomo_number % 2: # odd subtomos → forward direction # clamp end angle to max allowed - max_allowed_angle = 180.05 + self.tomo_angle_stepsize - proposed_end = start + 180 + max_allowed_angle = self.tomo_angle_range + 0.05 + self.tomo_angle_stepsize + proposed_end = start + self.tomo_angle_range angle_end = min(proposed_end, max_allowed_angle) angles = np.linspace(start, angle_end, num=N, endpoint=True) @@ -1756,7 +1806,7 @@ class Flomni( else: # even subtomos → reverse direction # go FROM start_angle down toward 0 min_allowed_angle = 0 - proposed_end = start - 180 + proposed_end = start - self.tomo_angle_range angle_end = max(proposed_end, min_allowed_angle) angles = np.linspace(start, angle_end, num=N, endpoint=True) @@ -1778,58 +1828,48 @@ class Flomni( if subtomo_number % 2: # odd = forward direction self._subtomo_offset = round(sa / step) else: # even = reverse direction - self._subtomo_offset = round((180 - sa) / step) + self._subtomo_offset = round((self.tomo_angle_range - sa) / step) # progress index must always increase self.progress["subtomo_projection"] = self._subtomo_offset + i # ------------------------------------------------------------ # existing progress fields - self.progress["subtomo_total_projections"] = int(180 / self.tomo_angle_stepsize) + self.progress["subtomo_total_projections"] = int( + self.tomo_angle_range / self.tomo_angle_stepsize + ) self.progress["projection"] = (subtomo_number - 1) * self.progress[ "subtomo_total_projections" ] + self.progress["subtomo_projection"] - self.progress["total_projections"] = 180 / self.tomo_angle_stepsize * 8 + self.progress["total_projections"] = ( + self.tomo_angle_range / self.tomo_angle_stepsize + ) * 8 self.progress["angle"] = angle # finally do the scan at this angle self._tomo_scan_at_angle(angle, subtomo_number) + @scan_repeat(max_repeats=10, default=True) def _tomo_scan_at_angle(self, angle, subtomo_number): - successful = False - error_caught = False - if 0 <= angle < 180.05: + + if 0 <= angle < self.tomo_angle_range + 0.05: self.progress["heartbeat"] = datetime.datetime.now().isoformat() print(f"Starting flOMNI scan for angle {angle} in subtomo {subtomo_number}") self._print_progress() - while not successful: - # self.bl_chk._bl_chk_start() - if not self.special_angles: - self._current_special_angles = [] - if self._current_special_angles: - next_special_angle = self._current_special_angles[0] - if np.isclose(angle, next_special_angle, atol=0.5): - self._current_special_angles.pop(0) - num_repeats = self.special_angle_repeats - else: - num_repeats = 1 - try: - start_scan_number = bec.queue.next_scan_number - for i in range(num_repeats): - self._at_each_angle(angle) - error_caught = False - except AlarmBase as exc: - if exc.alarm_type == "TimeoutError": - bec.queue.request_queue_reset() - time.sleep(2) - error_caught = True - else: - raise exc - # if self.bl_chk._bl_chk_stop() and not error_caught: - successful = True - # else: - # self.bl_chk._bl_chk_wait_until_recovered() + if not self.special_angles: + self._current_special_angles = [] + if self._current_special_angles: + next_special_angle = self._current_special_angles[0] + if np.isclose(angle, next_special_angle, atol=0.5): + self._current_special_angles.pop(0) + num_repeats = self.special_angle_repeats + else: + num_repeats = 1 + start_scan_number = bec.queue.next_scan_number + for i in range(num_repeats): + self._at_each_angle(angle) + end_scan_number = bec.queue.next_scan_number for scan_nr in range(start_scan_number, end_scan_number): self._write_tomo_scan_number(scan_nr, angle, subtomo_number) @@ -1851,6 +1891,10 @@ class Flomni( bec = builtins.__dict__.get("bec") scans = builtins.__dict__.get("scans") + + bec.builtin_actors.scan_interlock.trigger_setting = "restart_scan" + bec.builtin_actors.scan_interlock.enabled = True + self._current_special_angles = self.special_angles.copy() # a new tomo scan was started if ( @@ -1859,7 +1903,7 @@ class Flomni( or (self.tomo_type == 3 and projection_number == None) ): - #pylint: disable=undefined-variable + # pylint: disable=undefined-variable if bec.active_account != "": self.tomo_id = self.add_sample_database( self.sample_name, @@ -1875,6 +1919,11 @@ class Flomni( self.tomo_id = 0 self.write_pdf_report() self.progress["tomo_start_time"] = datetime.datetime.now().isoformat() + # reset stale estimates from any previous scan, otherwise the GUI + # would keep showing a leftover ETA from before this scan has + # accumulated enough projections to compute a fresh one + self.progress["estimated_remaining_time"] = None + self.progress["estimated_finish_time"] = None with scans.dataset_id_on_hold: if self.tomo_type == 1: @@ -2000,15 +2049,18 @@ class Flomni( projection = self.progress["projection"] total = self.progress["total_projections"] if start_str is not None and total > 0 and projection > 9: - elapsed = ( - datetime.datetime.now() - datetime.datetime.fromisoformat(start_str) - ).total_seconds() + now = datetime.datetime.now() + elapsed = (now - datetime.datetime.fromisoformat(start_str)).total_seconds() rate = projection / elapsed # projections per second remaining_s = (total - projection) / rate self.progress["estimated_remaining_time"] = remaining_s eta_str = self._format_duration(remaining_s) + finish_dt = now + datetime.timedelta(seconds=remaining_s) + self.progress["estimated_finish_time"] = finish_dt.isoformat() + finish_str = finish_dt.strftime("%Y-%m-%d %H:%M:%S") else: eta_str = "N/A" + finish_str = "N/A" # ---------------------------------------------------------------------- print("\x1b[95mProgress report:") print(f"Tomo type: ....................... {self.progress['tomo_type']}") @@ -2017,7 +2069,8 @@ class Flomni( print(f"Angle: ........................... {self.progress['angle']}") print(f"Current subtomo: ................. {self.progress['subtomo']}") print(f"Current projection within subtomo: {self.progress['subtomo_projection']}") - print(f"Estimated remaining time: ........ {eta_str}\x1b[0m") + print(f"Estimated remaining time: ........ {eta_str}") + print(f"Estimated finish time: ........... {finish_str}\x1b[0m") self._flomnigui_update_progress() def add_sample_database( @@ -2040,6 +2093,10 @@ class Flomni( flomni_at_each_angle(self, angle) return + if self.single_point_instead_of_fermat_scan: + self.tomo_acquire_at_angle(angle) + return + self.tomo_scan_projection(angle) def _golden(self, ii, howmany_sorted, maxangle, reverse=False): @@ -2123,6 +2180,7 @@ class Flomni( offsets[1] - self.compute_additional_correction_y(angle) - self.compute_additional_correction_y_2(angle) + + self.manual_shift_y ) sum_offset_z = offsets[2] @@ -2155,6 +2213,7 @@ class Flomni( zshift=sum_offset_z, angle=angle, exp_time=self.tomo_countingtime, + frames_per_trigger=self.frames_per_trigger, ) if self.corridor_size > 0: @@ -2164,6 +2223,67 @@ class Flomni( self.tomo_reconstruct() + def tomo_acquire_at_angle(self, angle: float, frames_per_trigger: int | None = None): + """ + Move fsamroy to `angle`, then move rtx/rty/rtz to the alignment-corrected + scan center (same alignment-offset logic as tomo_scan_projection, but + without stitching), and acquire a single frame or a burst via + scans.acquire instead of running a fermat scan. + + This mirrors the positioning sequence used internally by + flomni_fermat_scan (rotation, then rtx/rty/rtz with laser-tracker + on/check/move-to-region), but executes it as plain blocking + client-side calls, since this runs in the BEC client, not on the + scan server. + + Args: + angle (float): rotation angle [deg] to move fsamroy to. + frames_per_trigger (int, optional): number of burst frames for + this acquisition. Defaults to self.frames_per_trigger. + """ + scans = builtins.__dict__.get("scans") + + # --- rotation --- + fsamroy_current_setpoint = dev.fsamroy.user_setpoint.get() + if angle != fsamroy_current_setpoint: + umv(dev.fsamroy, angle) + else: + print("No rotation required") + + # --- alignment offset (same as tomo_scan_projection, no stitching) --- + offsets = self.get_alignment_offset(angle) + sum_offset_x = offsets[0] + sum_offset_y = ( + offsets[1] + - self.compute_additional_correction_y(angle) + - self.compute_additional_correction_y_2(angle) + + self.manual_shift_y + ) + sum_offset_z = offsets[2] + + # --- positioning + laser tracker, mirroring + # flomni_fermat_scan._prepare_setup_part2 --- + dev.rtx.controller.laser_tracker_on() + umv(dev.rtx, sum_offset_x, dev.rty, sum_offset_y, dev.rtz, sum_offset_z) + tracker_signal = dev.rtx.controller.laser_tracker_check_signalstrength() + # checks that the fsamx coarse stage is at a position that leaves + # sufficient piezo range on the fine (rtx) stage + dev.rtx.controller.move_samx_to_scan_region(sum_offset_x) + + if tracker_signal == "low": + logger.warning( + "Signal strength of the laser tracker is low. Realignment recommended!" + ) + elif tracker_signal == "toolow": + raise FlomniError( + "Signal strength of the laser tracker is too low for scanning. Realignment required!" + ) + + # --- acquire --- + n_frames = ( + frames_per_trigger if frames_per_trigger is not None else self.frames_per_trigger + ) + scans.acquire(exp_time=self.tomo_countingtime, frames_per_trigger=n_frames) def tomo_parameters(self): """print and update the tomo parameters""" @@ -2174,12 +2294,19 @@ class Flomni( print(f"Stitching number x,y = {self.stitch_x}, {self.stitch_y}") print(f"Stitching overlap = {self.tomo_stitch_overlap}") print(f"Reconstruction queue name = {self.ptycho_reconstruct_foldername}") - print(f" _manual_shift_y = {self.manual_shift_y}") + print(f" _manual_shift_y = {self.manual_shift_y}") + print(f"Frames per trigger (burst) = {self.frames_per_trigger}") + print(f"Single point instead of fermat = {self.single_point_instead_of_fermat_scan}") print("") if self.tomo_type == 1: print("\x1b[1mTomo type 1:\x1b[0m 8 equally spaced sub-tomograms") - print(f"Total number of projections: {180/self.tomo_angle_stepsize*8}") + print(f"Angular range = {self.tomo_angle_range} degrees") + print(f"Total number of projections: {(self.tomo_angle_range/self.tomo_angle_stepsize)*8}") print(f"Angular step within sub-tomogram: {self.tomo_angle_stepsize} degrees") + print( + "Angular step of the final (combined) tomogram:" + f" {self.tomo_angle_range/((self.tomo_angle_range/self.tomo_angle_stepsize)*8)} degrees" + ) if self.tomo_type == 2: print("\x1b[1mTomo type 2:\x1b[0m Golden ratio tomography") print(f"Sorted in bunches of: {self.golden_ratio_bunch_size}") @@ -2214,11 +2341,38 @@ class Flomni( self.tomo_shellstep = self._get_val(" um", self.tomo_shellstep, float) self.fovx = self._get_val(" um", self.fovx, float) self.fovy = self._get_val(" um", self.fovy, float) - self.stitch_x = self._get_val("", self.stitch_x, int) - self.stitch_y = self._get_val("", self.stitch_y, int) + if self.single_point_instead_of_fermat_scan: + print( + "Stitching is disabled while single point instead of fermat scan is" + " active; stitch X/Y forced to 0." + ) + self.stitch_x = 0 + self.stitch_y = 0 + else: + self.stitch_x = self._get_val("", self.stitch_x, int) + self.stitch_y = self._get_val("", self.stitch_y, int) self.ptycho_reconstruct_foldername = self._get_val( "Reconstruction queue ", self.ptycho_reconstruct_foldername, str ) + self.frames_per_trigger = self._get_val( + "Frames per trigger (burst)", self.frames_per_trigger, int + ) + self.single_point_instead_of_fermat_scan = bool( + self._get_val( + "Single point instead of fermat scan (acquire at angle) 1/0?", + int(self.single_point_instead_of_fermat_scan), + int, + ) + ) + if self.single_point_instead_of_fermat_scan and ( + self.stitch_x != 0 or self.stitch_y != 0 + ): + print( + "Stitching is not supported with single point instead of fermat scan;" + " stitch X/Y forced to 0." + ) + self.stitch_x = 0 + self.stitch_y = 0 print("Tomography type:") print(" 1: 8 equally spaced sub-tomograms") @@ -2227,12 +2381,22 @@ class Flomni( self.tomo_type = self._get_val("Tomography type", self.tomo_type, int) if self.tomo_type == 1: + self.tomo_angle_range = self._get_val( + "Angular range (180 or 360)", self.tomo_angle_range, int + ) tomo_numberofprojections = self._get_val( - "Total number of projections", 180 / self.tomo_angle_stepsize * 8, int + "Total number of projections", + (self.tomo_angle_range / self.tomo_angle_stepsize) * 8, + int, + ) + self.tomo_angle_stepsize = (self.tomo_angle_range / tomo_numberofprojections) * 8 + print( + f"The angular step within a sub-tomogram will be {self.tomo_angle_stepsize} degrees" + ) + print( + "The angular step of the final (combined) tomogram will be" + f" {self.tomo_angle_range / tomo_numberofprojections} degrees" ) - print(f"The angular step will be {180/tomo_numberofprojections}") - self.tomo_angle_stepsize = 180 / tomo_numberofprojections * 8 - print(f"The angular step in a subtomogram it will be {self.tomo_angle_stepsize}") if self.tomo_type == 2: self.golden_ratio_bunch_size = self._get_val( @@ -2313,11 +2477,11 @@ class Flomni( f"{'Dataset ID:':<{padding}}{dataset_id:>{padding}}\n", f"{'Sample Info:':<{padding}}{'Sample Info':>{padding}}\n", f"{'e-account:':<{padding}}{str(account):>{padding}}\n", - f"{'Number of projections:':<{padding}}{int(180 / self.tomo_angle_stepsize * 8):>{padding}}\n", + f"{'Number of projections:':<{padding}}{int((self.tomo_angle_range / self.tomo_angle_stepsize) * 8):>{padding}}\n", f"{'First scan number:':<{padding}}{self.client.queue.next_scan_number:>{padding}}\n", - f"{'Last scan number approx.:':<{padding}}{self.client.queue.next_scan_number + int(180 / self.tomo_angle_stepsize * 8) + 10:>{padding}}\n", + f"{'Last scan number approx.:':<{padding}}{self.client.queue.next_scan_number + int((self.tomo_angle_range / self.tomo_angle_stepsize) * 8) + 10:>{padding}}\n", f"{'Current photon energy:':<{padding}}To be implemented\n", - #f"{'Current photon energy:':<{padding}}{dev.mokev.read()['mokev']['value']:>{padding}.4f}\n", + # f"{'Current photon energy:':<{padding}}{dev.mokev.read()['mokev']['value']:>{padding}.4f}\n", f"{'Exposure time:':<{padding}}{self.tomo_countingtime:>{padding}.2f}\n", f"{'Fermat spiral step size:':<{padding}}{self.tomo_shellstep:>{padding}.2f}\n", f"{'FOV:':<{padding}}{fovxy:>{padding}}\n", @@ -2326,7 +2490,9 @@ class Flomni( f"{'Angular step within sub-tomogram:':<{padding}}{self.tomo_angle_stepsize:>{padding}.2f}\n", ] content = "".join(content) - user_target = os.path.expanduser(f"~/data/raw/documentation/tomo_scan_ID_{self.tomo_id}.pdf") + user_target = os.path.expanduser( + f"~/data/raw/documentation/tomo_scan_ID_{self.tomo_id}.pdf" + ) with PDFWriter(user_target) as file: file.write(header) file.write(content) @@ -2342,7 +2508,6 @@ class Flomni( # self.client.tomo_progress.send_tomo_progress_message("~/data/raw/documentation/tomo_scan_ID_{self.tomo_id}.pdf").send() import csaxs_bec - # Ensure this is a Path object, not a string csaxs_bec_basepath = Path(csaxs_bec.__file__) @@ -2350,14 +2515,12 @@ class Flomni( # Build the absolute path correctly logo_file = ( - csaxs_bec_basepath.parent - / "bec_ipython_client" - / "plugins" - / "flomni" - / logo_file_rel + csaxs_bec_basepath.parent / "bec_ipython_client" / "plugins" / "flomni" / logo_file_rel ).resolve() print(logo_file) - bec.messaging.scilog.new().add_attachment(logo_file, width=200).add_text(content.replace("\n", "
")).add_tags("tomoscan").send() + bec.messaging.scilog.new().add_attachment(logo_file, width=200).add_text( + content.replace("\n", "
") + ).add_tags("tomoscan").send() if __name__ == "__main__": diff --git a/csaxs_bec/device_configs/bl_detectors.yaml b/csaxs_bec/device_configs/bl_detectors.yaml index 3df5103..80a159e 100644 --- a/csaxs_bec/device_configs/bl_detectors.yaml +++ b/csaxs_bec/device_configs/bl_detectors.yaml @@ -13,8 +13,8 @@ eiger_9: description: Eiger 9M detector deviceClass: csaxs_bec.devices.jungfraujoch.eiger_9m.Eiger9M deviceConfig: - detector_distance: 100 - beam_center: [0, 0] + detector_distance: 2200 + beam_center: [870, 1203] onFailure: raise enabled: True readoutPriority: async diff --git a/csaxs_bec/device_configs/bl_endstation.yaml b/csaxs_bec/device_configs/bl_endstation.yaml index d9e6534..09e644a 100644 --- a/csaxs_bec/device_configs/bl_endstation.yaml +++ b/csaxs_bec/device_configs/bl_endstation.yaml @@ -1089,47 +1089,47 @@ bim_xbox3_slowrb: -# #################### -# ### Beamstop diode control for flight tube -# ### This requires galilrioft device. On top of that the gain control device is built as well as a slow voltage readback. -# #################### +#################### +### Beamstop diode control for flight tube +### This requires galilrioft device. On top of that the gain control device is built as well as a slow voltage readback. +#################### -# galilrioesft: -# description: Galil RIO for remote gain switching and slow reading FlightTube -# deviceClass: csaxs_bec.devices.omny.galil.galil_rio.GalilRIO -# deviceConfig: -# host: galilrioesft.psi.ch -# enabled: true -# onFailure: retry -# readOnly: false -# readoutPriority: baseline -# connectionTimeout: 20 +galilrioesft: + description: Galil RIO for remote gain switching and slow reading FlightTube + deviceClass: csaxs_bec.devices.omny.galil.galil_rio.GalilRIO + deviceConfig: + host: galilrioesft.psi.ch + enabled: true + onFailure: retry + readOnly: false + readoutPriority: baseline + connectionTimeout: 20 -# gain_beamstop_diode: -# description: Gain control for beamstop flightube -# deviceClass: csaxs_bec.devices.pseudo_devices.bpm_control.BPMControl -# deviceConfig: -# gain_lsb: galilrioesft.digital_out.ch0 # Pin 10 -> Galil ch0 -# gain_mid: galilrioesft.digital_out.ch1 # Pin 11 -> Galil ch1 -# gain_msb: galilrioesft.digital_out.ch2 # Pin 12 -> Galil ch2 -# coupling: galilrioesft.digital_out.ch3 # Pin 13 -> Galil ch3 -# speed_mode: galilrioesft.digital_out.ch4 # Pin 14 -> Galil ch4 -# enabled: true -# readoutPriority: baseline -# onFailure: retry -# needs: -# - galilrioesft +gain_beamstop_diode: + description: Gain control for beamstop flightube + deviceClass: csaxs_bec.devices.pseudo_devices.bpm_control.BPMControl + deviceConfig: + gain_lsb: galilrioesft.digital_out.ch0 # Pin 10 -> Galil ch0 + gain_mid: galilrioesft.digital_out.ch1 # Pin 11 -> Galil ch1 + gain_msb: galilrioesft.digital_out.ch2 # Pin 12 -> Galil ch2 + coupling: galilrioesft.digital_out.ch3 # Pin 13 -> Galil ch3 + speed_mode: galilrioesft.digital_out.ch4 # Pin 14 -> Galil ch4 + enabled: true + readoutPriority: baseline + onFailure: retry + needs: + - galilrioesft -# beamstop_intensity: -# description: Beamstop intensity from Galil analog input ch6 -# deviceClass: csaxs_bec.devices.pseudo_devices.signal_forwarder.SignalForwarder -# deviceConfig: -# signal: galilrioesft.analog_in.ch0 -# enabled: true -# readoutPriority: baseline -# onFailure: retry -# needs: -# - galilrioesft +beamstop_intensity: + description: Beamstop intensity from Galil analog input ch6 + deviceClass: csaxs_bec.devices.pseudo_devices.signal_forwarder.SignalForwarder + deviceConfig: + signal: galilrioesft.analog_in.ch0 + enabled: true + readoutPriority: monitored + onFailure: retry + needs: + - galilrioesft diff --git a/csaxs_bec/device_configs/bl_optics_hutch.yaml b/csaxs_bec/device_configs/bl_optics_hutch.yaml index 5d0edce..14bdaf3 100644 --- a/csaxs_bec/device_configs/bl_optics_hutch.yaml +++ b/csaxs_bec/device_configs/bl_optics_hutch.yaml @@ -264,6 +264,7 @@ scinx: # bl_smar_stage to use csaxs reference method. assign number according to axis channel init_position: -23 bl_smar_stage: 2 + in_position: -12.5 poly: description: polarizer holder in OPbox @@ -328,25 +329,25 @@ polrot: # readOnly: false # softwareTrigger: false -# #################### -# ### BPM and polarization diag XBox1 (optics hutch) -# ### This requires galilrioop device. On top of that the gain control devices and slow readback devices are built. -# ### dev.galilrioop.read() shows the analog inputs -# ### another example ...analog_in.ch0.get() -# ### dev.galilrioop.read_configuration() shows the digital channels -# ### example for direct access dev.galilrioesxbox.digital_out.ch1.put(0) -# #################### +#################### +### BPM and polarization diag XBox1 (optics hutch) +### This requires galilrioop device. On top of that the gain control devices and slow readback devices are built. +### dev.galilrioop.read() shows the analog inputs +### another example ...analog_in.ch0.get() +### dev.galilrioop.read_configuration() shows the digital channels +### example for direct access dev.galilrioesxbox.digital_out.ch1.put(0) +#################### -# galilrioop: -# description: Galil RIO for remote gain switching and slow reading XBox OP -# deviceClass: csaxs_bec.devices.omny.galil.galil_rio.GalilRIO -# deviceConfig: -# host: galilrioop.psi.ch -# enabled: true -# onFailure: retry -# readOnly: false -# readoutPriority: monitored -# connectionTimeout: 20 +galilrioop: + description: Galil RIO for remote gain switching and slow reading XBox OP + deviceClass: csaxs_bec.devices.omny.galil.galil_rio.GalilRIO + deviceConfig: + host: galilrioop.psi.ch + enabled: true + onFailure: retry + readOnly: false + readoutPriority: monitored + connectionTimeout: 20 # gain_bpm_xbox1: # description: Gain control for BPM XBox1 (OP hutch) @@ -377,42 +378,42 @@ polrot: # needs: # - galilrioop -# gain_diodes_xbox1: -# description: Gain control for diodes (horizontal and vertical) XBox1 -# deviceClass: csaxs_bec.devices.pseudo_devices.bpm_control.BPMControl -# deviceConfig: -# gain_lsb: galilrioop.digital_out.ch6 # Pin 10 -> Galil ch0 -# gain_mid: galilrioop.digital_out.ch7 # Pin 11 -> Galil ch1 -# gain_msb: galilrioop.digital_out.ch8 # Pin 12 -> Galil ch2 -# coupling: galilrioop.digital_out.ch9 # Pin 13 -> Galil ch3 -# speed_mode: galilrioop.digital_out.ch10 # Pin 14 -> Galil ch4 -# enabled: true -# readoutPriority: baseline -# onFailure: retry -# needs: -# - galilrioop +gain_diodes_xbox1: + description: Gain control for diodes (horizontal and vertical) XBox1 + deviceClass: csaxs_bec.devices.pseudo_devices.bpm_control.BPMControl + deviceConfig: + gain_lsb: galilrioop.digital_out.ch6 # Pin 10 -> Galil ch0 + gain_mid: galilrioop.digital_out.ch7 # Pin 11 -> Galil ch1 + gain_msb: galilrioop.digital_out.ch8 # Pin 12 -> Galil ch2 + coupling: galilrioop.digital_out.ch9 # Pin 13 -> Galil ch3 + speed_mode: galilrioop.digital_out.ch10 # Pin 14 -> Galil ch4 + enabled: true + readoutPriority: baseline + onFailure: retry + needs: + - galilrioop -# diode_horizontal_xbox1_slowrb: -# description: Slow readback diode horizontal XBox OP (polarization diagnostics) -# deviceClass: csaxs_bec.devices.pseudo_devices.signal_forwarder.SignalForwarder -# deviceConfig: -# signal: galilrioop.analog_in.ch6 -# enabled: true -# readoutPriority: baseline -# onFailure: retry -# needs: -# - galilrioop +diode_horizontal_xbox1_slowrb: + description: Slow readback diode horizontal XBox OP (polarization diagnostics) + deviceClass: csaxs_bec.devices.pseudo_devices.signal_forwarder.SignalForwarder + deviceConfig: + signal: galilrioop.analog_in.ch6 + enabled: true + readoutPriority: monitored + onFailure: retry + needs: + - galilrioop -# diode_vertical_xbox1_slowrb: -# description: Slow readback diode vertical XBox OP (polarization diagnostics) -# deviceClass: csaxs_bec.devices.pseudo_devices.signal_forwarder.SignalForwarder -# deviceConfig: -# signal: galilrioop.analog_in.ch7 -# enabled: true -# readoutPriority: baseline -# onFailure: retry -# needs: -# - galilrioop +diode_vertical_xbox1_slowrb: + description: Slow readback diode vertical XBox OP (polarization diagnostics) + deviceClass: csaxs_bec.devices.pseudo_devices.signal_forwarder.SignalForwarder + deviceConfig: + signal: galilrioop.analog_in.ch7 + enabled: true + readoutPriority: monitored + onFailure: retry + needs: + - galilrioop sl3xi: description: "slit 2 (optics) x ring" @@ -525,3 +526,339 @@ sl3ys: deviceTags: - cSAXS - optics + +kbvbendu: + description: "KB Vertical Focusing Mirror, bender upstream" + deviceClass: ophyd_devices.EpicsMotorEC + deviceConfig: + prefix: "X12SA-OP-VFM:BNDU" + onFailure: raise + enabled: true + readoutPriority: baseline + readOnly: false + softwareTrigger: false + deviceTags: + - cSAXS + - optics + +kbvbendd: + description: "KB Vertical Focusing Mirror, bender downstream" + deviceClass: ophyd_devices.EpicsMotorEC + deviceConfig: + prefix: "X12SA-OP-VFM:BNDD" + onFailure: raise + enabled: true + readoutPriority: baseline + readOnly: false + softwareTrigger: false + deviceTags: + - cSAXS + - optics + +kbvtrx: + description: "KB Vertical Focusing Mirror, translation X" + deviceClass: ophyd_devices.EpicsMotorEC + deviceConfig: + prefix: "X12SA-OP-VFM:TRX" + onFailure: raise + enabled: true + readoutPriority: baseline + readOnly: false + softwareTrigger: false + deviceTags: + - cSAXS + - optics + +kbvtry: + description: "KB Vertical Focusing Mirror, translation Y" + deviceClass: ophyd_devices.EpicsMotorEC + deviceConfig: + prefix: "X12SA-OP-VFM:TRY" + onFailure: raise + enabled: true + readoutPriority: baseline + readOnly: false + softwareTrigger: false + deviceTags: + - cSAXS + - optics + +kbvyaw: + description: "KB Vertical Focusing Mirror, yaw" + deviceClass: ophyd_devices.EpicsMotorEC + deviceConfig: + prefix: "X12SA-OP-VFM:YAW" + onFailure: raise + enabled: true + readoutPriority: baseline + readOnly: false + softwareTrigger: false + deviceTags: + - cSAXS + - optics + +kbvroll: + description: "KB Vertical Focusing Mirror, roll" + deviceClass: ophyd_devices.EpicsMotorEC + deviceConfig: + prefix: "X12SA-OP-VFM:ROLL" + onFailure: raise + enabled: true + readoutPriority: baseline + readOnly: false + softwareTrigger: false + deviceTags: + - cSAXS + - optics + +kbvpitch: + description: "KB Vertical Focusing Mirror, pitch" + deviceClass: ophyd_devices.EpicsMotorEC + deviceConfig: + prefix: "X12SA-OP-VFM:PITCH" + onFailure: raise + enabled: true + readoutPriority: baseline + readOnly: false + softwareTrigger: false + deviceTags: + - cSAXS + - optics + +kbvtrxu: + description: "KB Vertical Focusing Mirror, translation X upstream" + deviceClass: ophyd_devices.EpicsMotorEC + deviceConfig: + prefix: "X12SA-OP-VFM:TRXU" + onFailure: raise + enabled: true + readoutPriority: baseline + readOnly: false + softwareTrigger: false + deviceTags: + - cSAXS + - optics + +kbvtrxd: + description: "KB Vertical Focusing Mirror, translation X downstream" + deviceClass: ophyd_devices.EpicsMotorEC + deviceConfig: + prefix: "X12SA-OP-VFM:TRXD" + onFailure: raise + enabled: true + readoutPriority: baseline + readOnly: false + softwareTrigger: false + deviceTags: + - cSAXS + - optics + +kbvtryur: + description: "KB Vertical Focusing Mirror, translation Y upstream ring" + deviceClass: ophyd_devices.EpicsMotorEC + deviceConfig: + prefix: "X12SA-OP-VFM:TRYUR" + onFailure: raise + enabled: true + readoutPriority: baseline + readOnly: false + softwareTrigger: false + deviceTags: + - cSAXS + - optics + +kbvtryw: + description: "KB Vertical Focusing Mirror, translation Y wall" + deviceClass: ophyd_devices.EpicsMotorEC + deviceConfig: + prefix: "X12SA-OP-VFM:TRYW" + onFailure: raise + enabled: true + readoutPriority: baseline + readOnly: false + softwareTrigger: false + deviceTags: + - cSAXS + - optics + +kbvtrydr: + description: "KB Vertical Focusing Mirror, translation Y downstream ring" + deviceClass: ophyd_devices.EpicsMotorEC + deviceConfig: + prefix: "X12SA-OP-VFM:TRYDR" + onFailure: raise + enabled: true + readoutPriority: baseline + readOnly: false + softwareTrigger: false + deviceTags: + - cSAXS + - optics + +kbhbendu: + description: "KB Horizontal Focusing Mirror, bender upstream" + deviceClass: ophyd_devices.EpicsMotorEC + deviceConfig: + prefix: "X12SA-OP-HFM:BNDU" + onFailure: raise + enabled: true + readoutPriority: baseline + readOnly: false + softwareTrigger: false + deviceTags: + - cSAXS + - optics + +kbhbendd: + description: "KB Horizontal Focusing Mirror, bender downstream" + deviceClass: ophyd_devices.EpicsMotorEC + deviceConfig: + prefix: "X12SA-OP-HFM:BNDD" + onFailure: raise + enabled: true + readoutPriority: baseline + readOnly: false + softwareTrigger: false + deviceTags: + - cSAXS + - optics + +kbhtrx: + description: "KB Horizontal Focusing Mirror, translation X" + deviceClass: ophyd_devices.EpicsMotorEC + deviceConfig: + prefix: "X12SA-OP-HFM:TRX" + onFailure: raise + enabled: true + readoutPriority: baseline + readOnly: false + softwareTrigger: false + deviceTags: + - cSAXS + - optics + +kbhtry: + description: "KB Horizontal Focusing Mirror, translation Y" + deviceClass: ophyd_devices.EpicsMotorEC + deviceConfig: + prefix: "X12SA-OP-HFM:TRY" + onFailure: raise + enabled: true + readoutPriority: baseline + readOnly: false + softwareTrigger: false + deviceTags: + - cSAXS + - optics + +kbhyaw: + description: "KB Horizontal Focusing Mirror, yaw" + deviceClass: ophyd_devices.EpicsMotorEC + deviceConfig: + prefix: "X12SA-OP-HFM:YAW" + onFailure: raise + enabled: true + readoutPriority: baseline + readOnly: false + softwareTrigger: false + deviceTags: + - cSAXS + - optics + +kbhroll: + description: "KB Horizontal Focusing Mirror, roll" + deviceClass: ophyd_devices.EpicsMotorEC + deviceConfig: + prefix: "X12SA-OP-HFM:ROLL" + onFailure: raise + enabled: true + readoutPriority: baseline + readOnly: false + softwareTrigger: false + deviceTags: + - cSAXS + - optics + +kbhpitch: + description: "KB Horizontal Focusing Mirror, pitch" + deviceClass: ophyd_devices.EpicsMotorEC + deviceConfig: + prefix: "X12SA-OP-HFM:PITCH" + onFailure: raise + enabled: true + readoutPriority: baseline + readOnly: false + softwareTrigger: false + deviceTags: + - cSAXS + - optics + +kbhtrxu: + description: "KB Horizontal Focusing Mirror, translation X upstream" + deviceClass: ophyd_devices.EpicsMotorEC + deviceConfig: + prefix: "X12SA-OP-HFM:TRXU" + onFailure: raise + enabled: true + readoutPriority: baseline + readOnly: false + softwareTrigger: false + deviceTags: + - cSAXS + - optics + +kbhtrxd: + description: "KB Horizontal Focusing Mirror, translation X downstream" + deviceClass: ophyd_devices.EpicsMotorEC + deviceConfig: + prefix: "X12SA-OP-HFM:TRXD" + onFailure: raise + enabled: true + readoutPriority: baseline + readOnly: false + softwareTrigger: false + deviceTags: + - cSAXS + - optics + +kbhtryuw: + description: "KB Horizontal Focusing Mirror, translation Y upstream wall" + deviceClass: ophyd_devices.EpicsMotorEC + deviceConfig: + prefix: "X12SA-OP-HFM:TRYUW" + onFailure: raise + enabled: true + readoutPriority: baseline + readOnly: false + softwareTrigger: false + deviceTags: + - cSAXS + - optics + +kbhtryr: + description: "KB Horizontal Focusing Mirror, translation Y ring" + deviceClass: ophyd_devices.EpicsMotorEC + deviceConfig: + prefix: "X12SA-OP-HFM:TRYR" + onFailure: raise + enabled: true + readoutPriority: baseline + readOnly: false + softwareTrigger: false + deviceTags: + - cSAXS + - optics + +kbhtrydw: + description: "KB Horizontal Focusing Mirror, translation Y downstream wall" + deviceClass: ophyd_devices.EpicsMotorEC + deviceConfig: + prefix: "X12SA-OP-HFM:TRYDW" + onFailure: raise + enabled: true + readoutPriority: baseline + readOnly: false + softwareTrigger: false + deviceTags: + - cSAXS + - optics diff --git a/csaxs_bec/device_configs/ptycho_flomni.yaml b/csaxs_bec/device_configs/ptycho_flomni.yaml index 78296a1..5321e12 100644 --- a/csaxs_bec/device_configs/ptycho_flomni.yaml +++ b/csaxs_bec/device_configs/ptycho_flomni.yaml @@ -437,7 +437,7 @@ cam_xeye: description: Camera flOMNI Xray eye ID1 deviceClass: csaxs_bec.devices.ids_cameras.ids_camera.IDSCamera deviceConfig: - camera_id: 1 + camera_id: 11 bits_per_pixel: 24 num_rotation_90: 3 transpose: false @@ -511,33 +511,33 @@ calculated_signal: ############################################################ #################### OMNY Pandabox ######################### ############################################################ -omny_panda: - readoutPriority: async - deviceClass: csaxs_bec.devices.panda_box.panda_box_omny.PandaBoxOMNY - deviceConfig: - host: omny-panda.psi.ch - signal_alias: - FMC_IN.VAL1.Min: cap_voltage_fzp_y_min - FMC_IN.VAL1.Max: cap_voltage_fzp_y_max - FMC_IN.VAL1.Mean: cap_voltage_fzp_y_mean - FMC_IN.VAL2.Min: cap_voltage_fzp_x_min - FMC_IN.VAL2.Max: cap_voltage_fzp_x_max - FMC_IN.VAL2.Mean: cap_voltage_fzp_x_mean - INENC1.VAL.Max: interf_st_fzp_y_max - INENC1.VAL.Mean: interf_st_fzp_y_mean - INENC1.VAL.Min: interf_st_fzp_y_min - INENC2.VAL.Max: interf_st_fzp_x_max - INENC2.VAL.Mean: interf_st_fzp_x_mean - INENC2.VAL.Min: interf_st_fzp_x_min - INENC3.VAL.Max: interf_st_rotz_max - INENC3.VAL.Mean: interf_st_rotz_mean - INENC3.VAL.Min: interf_st_rotz_min - INENC4.VAL.Max: interf_st_rotx_max - INENC4.VAL.Mean: interf_st_rotx_mean - INENC4.VAL.Min: interf_st_rotx_min - PCAP.GATE_DURATION.Value: pcap_gate_duration_value - deviceTags: - - detector - enabled: true - readOnly: false - softwareTrigger: false +# omny_panda: +# readoutPriority: async +# deviceClass: csaxs_bec.devices.panda_box.panda_box_omny.PandaBoxOMNY +# deviceConfig: +# host: omny-panda.psi.ch +# signal_alias: +# FMC_IN.VAL1.Min: cap_voltage_fzp_y_min +# FMC_IN.VAL1.Max: cap_voltage_fzp_y_max +# FMC_IN.VAL1.Mean: cap_voltage_fzp_y_mean +# FMC_IN.VAL2.Min: cap_voltage_fzp_x_min +# FMC_IN.VAL2.Max: cap_voltage_fzp_x_max +# FMC_IN.VAL2.Mean: cap_voltage_fzp_x_mean +# INENC1.VAL.Max: interf_st_fzp_y_max +# INENC1.VAL.Mean: interf_st_fzp_y_mean +# INENC1.VAL.Min: interf_st_fzp_y_min +# INENC2.VAL.Max: interf_st_fzp_x_max +# INENC2.VAL.Mean: interf_st_fzp_x_mean +# INENC2.VAL.Min: interf_st_fzp_x_min +# INENC3.VAL.Max: interf_st_rotz_max +# INENC3.VAL.Mean: interf_st_rotz_mean +# INENC3.VAL.Min: interf_st_rotz_min +# INENC4.VAL.Max: interf_st_rotx_max +# INENC4.VAL.Mean: interf_st_rotx_mean +# INENC4.VAL.Min: interf_st_rotx_min +# PCAP.GATE_DURATION.Value: pcap_gate_duration_value +# deviceTags: +# - detector +# enabled: true +# readOnly: false +# softwareTrigger: false diff --git a/csaxs_bec/device_configs/simulated_omny/simulated_bl_endstation.yaml b/csaxs_bec/device_configs/simulated_omny/simulated_bl_endstation.yaml index 2ac5201..5deb8c7 100644 --- a/csaxs_bec/device_configs/simulated_omny/simulated_bl_endstation.yaml +++ b/csaxs_bec/device_configs/simulated_omny/simulated_bl_endstation.yaml @@ -18,4 +18,7 @@ fsh: readoutPriority: monitored flomni: - - !include ../ptycho_flomni.yaml \ No newline at end of file + - !include ../ptycho_flomni.yaml + +machine: + - !include ../machine.yml diff --git a/csaxs_bec/devices/jungfraujoch/eiger.py b/csaxs_bec/devices/jungfraujoch/eiger.py index e8d94d2..eccb857 100644 --- a/csaxs_bec/devices/jungfraujoch/eiger.py +++ b/csaxs_bec/devices/jungfraujoch/eiger.py @@ -36,6 +36,7 @@ import os import time from typing import TYPE_CHECKING, Literal +import numpy as np import yaml from bec_lib.file_utils import get_full_path from bec_lib.logger import bec_logger @@ -117,6 +118,8 @@ class Eiger(PSIDeviceBase): self._full_path = "" self._num_triggers = 0 self._wait_for_on_complete = 20 # seconds + # Initial incident energy in keV, if restarted None + self._incident_energy: float | None = None if self.device_manager is not None: self.device_manager: DeviceManagerDS self.scan_parameters: ScanServerScanInfo | None = None @@ -272,7 +275,25 @@ class Eiger(PSIDeviceBase): # TODO: Check mono energy from device in BEC # Setting incident energy in keV - incident_energy = 12.0 + + try: + incident_energy = self._get_beam_energy(self.device_manager) + if self._incident_energy is None: + self._incident_energy = round(float(incident_energy), 3) + elif not np.isclose( + self._incident_energy, incident_energy, atol=0.01 + ): # 10 keV tolerance + logger.warning( + f"Incident energy changed from {self._incident_energy} keV to {incident_energy} keV for device {self.name}. " + ) + self._incident_energy = round(float(incident_energy), 3) + except Exception as e: + logger.warning(f"Failed to set up beam energy for {self.name}: {e}") + incident_energy = 12.0 # default to 12 keV if error occurs + self._incident_energy = round(float(incident_energy), 3) + + logger.info(f"Device {self.name} uses incident energy of {self._incident_energy} keV.") + # Setting up exp_time and num_triggers acquisition parameter exp_time = self.scan_parameters.exp_time if exp_time <= self._readout_time: # Exp_time must be at least the readout time @@ -417,3 +438,20 @@ class Eiger(PSIDeviceBase): self.jfj_preview_client.stop() self.on_stop() return super().on_destroy() + + def _get_beam_energy(self, device_manager: DeviceManagerDS) -> float: + """ + Fetch the beam energy from the device manager. + + Args: + device_manager (DeviceManagerDS): The device manager to fetch the beam energy from. + + Returns: + float: The beam energy in keV. + """ + if hasattr(device_manager, "devices") and hasattr(device_manager.devices, "ccm_energy"): + energy = device_manager.devices.ccm_energy.read()[ + device_manager.devices.ccm_energy.name + ]["value"] + + return energy