diff --git a/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni.py b/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni.py index b7000253..f9d26d4b 100644 --- a/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni.py +++ b/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni.py @@ -2756,10 +2756,13 @@ class LamNI( except Exception: focal_distance_str = "N/A" # FZP focus-to-sample distance: same live z-stage-based calculation - # lfzp_info() already uses -- not a stored value. + # lfzp_info() already uses -- not a stored value. Depends on the + # mounted FZP type (30 nm sits in an extra holder further downstream). try: loptz_val = dev.loptz.read()["loptz"]["value"] - fzp_sample_distance_str = f"{-loptz_val + 85.6 + 52:.1f}" + fzp_sample_distance_str = ( + f"{self._fzp_sample_distance(loptz_val, fzp_zone_width_nm):.1f}" + ) except Exception: fzp_sample_distance_str = "N/A" # Sample-to-detector distance: userParameter on loptx, defaults to diff --git a/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni_optics_mixin.py b/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni_optics_mixin.py index fa7602c5..9032b687 100644 --- a/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni_optics_mixin.py +++ b/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni_optics_mixin.py @@ -214,21 +214,65 @@ class LamNIOpticsMixin: return None return param.get(var) - @property - def _sl1xs_before_eye(self): - return self.client.get_global_var("lamni_sl1xs_before_eye") + # slits opened by _EYE_SLIT_OPENING (relative) while the xray eye is in + _EYE_SLITS = ("sl1", "sl6") + _EYE_SLIT_OPENING = 1.0 # mm - @_sl1xs_before_eye.setter - def _sl1xs_before_eye(self, val): - self.client.set_global_var("lamni_sl1xs_before_eye", val) + @staticmethod + def _slit_size(name): + # sl1xs/sl1ys are EpicsMotors (user_readback), sl6xs/sl6ys are + # VirtualSlitWidth pseudo motors (readback) + device = dev[name] + if hasattr(device, "user_readback"): + return device.user_readback.get() + return device.readback.get() - @property - def _sl1ys_before_eye(self): - return self.client.get_global_var("lamni_sl1ys_before_eye") + def _slit_before_eye(self, slit): + return ( + self.client.get_global_var(f"lamni_{slit}xs_before_eye"), + self.client.get_global_var(f"lamni_{slit}ys_before_eye"), + ) - @_sl1ys_before_eye.setter - def _sl1ys_before_eye(self, val): - self.client.set_global_var("lamni_sl1ys_before_eye", val) + def _set_slit_before_eye(self, slit, xs, ys): + self.client.set_global_var(f"lamni_{slit}xs_before_eye", xs) + self.client.set_global_var(f"lamni_{slit}ys_before_eye", ys) + + def _open_slits_for_eye(self): + for slit in self._EYE_SLITS: + xs, ys = f"{slit}xs", f"{slit}ys" + if xs not in dev or ys not in dev: + continue + self._set_slit_before_eye(slit, self._slit_size(xs), self._slit_size(ys)) + scans.umv( + dev[xs], self._EYE_SLIT_OPENING, dev[ys], self._EYE_SLIT_OPENING, relative=True + ) + + def _restore_slits_after_eye(self): + for slit in self._EYE_SLITS: + xs, ys = f"{slit}xs", f"{slit}ys" + xs_before, ys_before = self._slit_before_eye(slit) + if xs_before is None or ys_before is None: + continue + if xs not in dev or ys not in dev: + self._set_slit_before_eye(slit, None, None) + continue + # if the slit was manually re-adjusted while the eye was in, + # offer to keep the current size instead of restoring the + # pre-eye value + tol = 0.01 # mm (10 microns) + manually_adjusted = not np.isclose( + self._slit_size(xs), xs_before + self._EYE_SLIT_OPENING, atol=tol + ) or not np.isclose(self._slit_size(ys), ys_before + self._EYE_SLIT_OPENING, atol=tol) + + if manually_adjusted and self.OMNYTools.yesno( + f"{xs}/{ys} appear to have been adjusted manually while the " + "eye was in. Keep the current slit size instead of " + "restoring the pre-eye value?" + ): + print(f"Keeping current {slit} size, not restoring pre-eye value.") + else: + umv(dev[xs], xs_before, dev[ys], ys_before) + self._set_slit_before_eye(slit, None, None) def leye_out(self): self.loptics_in() @@ -240,31 +284,7 @@ class LamNIOpticsMixin: if self.csaxs is not None: self.csaxs.fil_trans(1, print_only=False) - sl1xs_before = self._sl1xs_before_eye - sl1ys_before = self._sl1ys_before_eye - if sl1xs_before is not None and sl1ys_before is not None: - # if the slit was manually re-adjusted while the eye was in, - # offer to keep the current size instead of restoring the - # pre-eye value - current_sl1xs = dev.sl1xs.user_readback.get() - current_sl1ys = dev.sl1ys.user_readback.get() - expected_sl1xs = sl1xs_before + 0.3 - expected_sl1ys = sl1ys_before + 0.3 - tol = 0.01 # mm (10 microns) - manually_adjusted = not np.isclose( - current_sl1xs, expected_sl1xs, atol=tol - ) or not np.isclose(current_sl1ys, expected_sl1ys, atol=tol) - - if manually_adjusted and self.OMNYTools.yesno( - "sl1xs/sl1ys appear to have been adjusted manually while the " - "eye was in. Keep the current slit size instead of " - "restoring the pre-eye value?" - ): - print("Keeping current slit size, not restoring pre-eye value.") - else: - umv(dev.sl1xs, sl1xs_before, dev.sl1ys, sl1ys_before) - self._sl1xs_before_eye = None - self._sl1ys_before_eye = None + self._restore_slits_after_eye() leyey_out = self._get_user_param_safe("leyey", "out") umv(dev.leyey, leyey_out) @@ -318,10 +338,7 @@ class LamNIOpticsMixin: # both already in position -> do nothing pass else: - if "sl1xs" in dev and "sl1ys" in dev: - self._sl1xs_before_eye = dev.sl1xs.user_readback.get() - self._sl1ys_before_eye = dev.sl1ys.user_readback.get() - scans.umv(dev.sl1xs, 0.3, dev.sl1ys, 0.3, relative=True) + self._open_slits_for_eye() if self.csaxs is not None: self.csaxs.fil_trans(0.05, print_only=False) @@ -539,6 +556,17 @@ class LamNIOpticsMixin: else: print("OSA is already at the OUT position.") + # the 30 nm FZP sits in an additional holder that puts the chip this + # much further downstream (closer to the sample) than the 60 nm FZP + _FZP_30NM_HOLDER_OFFSET = 11.0 # mm + + def _fzp_sample_distance(self, loptz_val, outermost_zone_width_nm): + """FZP-to-sample distance in mm for the given loptz position and FZP type.""" + distance = -loptz_val + 85.6 + 52 + if outermost_zone_width_nm == 30: + distance -= self._FZP_30NM_HOLDER_OFFSET + return distance + def lfzp_info(self, mokev_val=-1): if mokev_val == -1: # The energy readback device is "ccm_energy" (same as flomni's @@ -553,8 +581,13 @@ class LamNIOpticsMixin: ) return loptz_val = dev.loptz.read()["loptz"]["value"] - distance = -loptz_val + 85.6 + 52 - print(f"The sample is in a distance of {distance:.1f} mm from the FZP.") + distance = self._fzp_sample_distance(loptz_val, 60) + distance_30nm = self._fzp_sample_distance(loptz_val, 30) + print( + f"The sample is in a distance of {distance:.1f} mm from a 60 nm FZP and " + f"{distance_30nm:.1f} mm from a 30 nm FZP (extra holder, " + f"{self._FZP_30NM_HOLDER_OFFSET:.0f} mm further downstream)." + ) print(f"At the current energy of {mokev_val:.4f} keV we have following options:\n") @@ -597,7 +630,7 @@ class LamNIOpticsMixin: outermost_zonewidth = 30e-9 focal_distance = diameter * outermost_zonewidth / wavelength beam_size = ( - -diameter / (focal_distance * 1000) * (focal_distance * 1000 - distance) * 1e6 + -diameter / (focal_distance * 1000) * (focal_distance * 1000 - distance_30nm) * 1e6 ) table.add_row( f"{diameter*1e6:.2f} microns", @@ -633,7 +666,7 @@ class LamNIOpticsMixin: print(f" Current losaz {losaz_val:.1f}") print( " The OSA will collide with a normal sample holder at losaz " - f"\033[1m{(collision_offset - losaz_val):.1f}\033[0m" + f"\033[1m{(collision_offset - loptz_val):.1f}\033[0m" ) print(f" Remaining space (right now): \033[1m{remaining_current:.1f}\033[0m") diff --git a/csaxs_bec/device_configs/ptycho_lamni.yaml b/csaxs_bec/device_configs/ptycho_lamni.yaml index ab489b82..d4487abc 100644 --- a/csaxs_bec/device_configs/ptycho_lamni.yaml +++ b/csaxs_bec/device_configs/ptycho_lamni.yaml @@ -22,9 +22,9 @@ leyex: in: 13.18 # flight tube / detector table positions used by leye_out() (*_in) and # leye_in() (*_out) - fttrz_in: 1995 + fttrz_in: 1381.433 fttrz_out: 2000 - dttrz_in: 13258 + dttrz_in: 12614.99 dttrz_out: 13263 deviceTags: - ptycho_lamni @@ -111,8 +111,8 @@ loptz: axis_Id: D host: mpc2680.psi.ch limits: - - 0 - - 0 + - 80 + - 89 port: 8081 sign: -1 enabled: true @@ -120,6 +120,10 @@ loptz: readOnly: false readoutPriority: baseline connectionTimeout: 20 + userParameter: + # OSA collides with the sample holder when loptz + losaz reaches this + # value (measured 2026-10-14: loptz 77.8676, losaz 10.0029) + collision_offset: 87.87 deviceTags: - ptycho_lamni lsamrot: @@ -203,7 +207,7 @@ losax: readoutPriority: baseline connectionTimeout: 20 userParameter: - in: -1.582 + in: -1.66249 deviceTags: - ptycho_lamni losay: @@ -223,7 +227,7 @@ losay: readoutPriority: baseline connectionTimeout: 20 userParameter: - in: -0.131 + in: -0.236 out: 3.8 deviceTags: - ptycho_lamni @@ -244,7 +248,7 @@ losaz: readoutPriority: baseline connectionTimeout: 20 userParameter: - in: -1 + in: 8.829 out: -3 deviceTags: - ptycho_lamni