diff --git a/csaxs_bec/bec_ipython_client/plugins/flomni/flomni.py b/csaxs_bec/bec_ipython_client/plugins/flomni/flomni.py index 9d4cd64..b293a1c 100644 --- a/csaxs_bec/bec_ipython_client/plugins/flomni/flomni.py +++ b/csaxs_bec/bec_ipython_client/plugins/flomni/flomni.py @@ -1578,6 +1578,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 +1616,35 @@ 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): + 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) @@ -1729,14 +1774,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 +1792,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 +1801,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,18 +1823,20 @@ 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 @@ -1798,7 +1845,7 @@ class Flomni( @scan_repeat(max_repeats=10, default=True) def _tomo_scan_at_angle(self, angle, subtomo_number): - 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() @@ -2026,6 +2073,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): @@ -2141,6 +2192,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: @@ -2150,6 +2202,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) + ) + 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""" print("Current settings:") @@ -2160,10 +2273,13 @@ class Flomni( 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"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") if self.tomo_type == 2: print("\x1b[1mTomo type 2:\x1b[0m Golden ratio tomography") @@ -2204,6 +2320,16 @@ class Flomni( 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, + ) + ) print("Tomography type:") print(" 1: 8 equally spaced sub-tomograms") @@ -2212,12 +2338,16 @@ class Flomni( self.tomo_type = self._get_val("Tomography type", self.tomo_type, int) if self.tomo_type == 1: - tomo_numberofprojections = self._get_val( - "Total number of projections", 180 / self.tomo_angle_stepsize * 8, int + self.tomo_angle_range = self._get_val( + "Angular range (180 or 360)", self.tomo_angle_range, int ) - 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}") + tomo_numberofprojections = self._get_val( + "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 of the final tomogram will be {self.tomo_angle_stepsize} degrees") if self.tomo_type == 2: self.golden_ratio_bunch_size = self._get_val( @@ -2298,9 +2428,9 @@ 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"{'Exposure time:':<{padding}}{self.tomo_countingtime:>{padding}.2f}\n",