feat(flomni): add 360° tomo range and single-point acquire mode
Add optional full-circle (0-360°, inclusive) angular sweep for tomo_type 1 (equally spaced sub-tomograms), selectable via the new tomo_angle_range property (180 default, or 360). Fixes the _tomo_scan_at_angle gate, which previously hardcoded a 180.05° cutoff and would silently drop any projection beyond 180° without error. Add an alternative acquisition mode for each projection angle: tomo_acquire_at_angle moves fsamroy and the alignment-corrected rtx/rty/rtz position (same offset logic as tomo_scan_projection, no stitching), runs the existing laser-tracker checks and move_samx_to_scan_region, then calls scans.acquire instead of scans.flomni_fermat_scan. Selectable per scan via the new single_point_instead_of_fermat_scan flag, dispatched from _at_each_angle, so it applies independently of tomo_type and tomo_angle_range. Add frames_per_trigger as a persistent property, used by both scans.flomni_fermat_scan (already supported it) and the new scans.acquire call, for burst acquisition. Surface all three new settings in tomo_parameters()'s printout and interactive editor. Update write_pdf_report's projection-count estimate to scale with tomo_angle_range instead of a hardcoded 180. tomo_alignment_scan and tomo_type 2/3 angle generation remain unchanged (still 0-180°), as the new range option only applies to tomo_type 1.
This commit is contained in:
@@ -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 <mm> = {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",
|
||||
|
||||
Reference in New Issue
Block a user