Flomni commissioning 3 #240
@@ -1,23 +1,21 @@
|
||||
# import builtins
|
||||
# import datetime
|
||||
# import os
|
||||
# import subprocess
|
||||
# import time
|
||||
# from pathlib import Path
|
||||
import inspect
|
||||
|
||||
# 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 typeguard import typechecked
|
||||
|
||||
|
||||
from csaxs_bec.bec_ipython_client.plugins.cSAXS.diagnostics import cSAXSDiagnostics
|
||||
from csaxs_bec.bec_ipython_client.plugins.cSAXS.filter_transmission import cSAXSFilterTransmission
|
||||
from csaxs_bec.bec_ipython_client.plugins.cSAXS.intensity_map_predict_gap import (
|
||||
predict_gap as _predict_gap,
|
||||
)
|
||||
from csaxs_bec.bec_ipython_client.plugins.cSAXS.slits import cSAXSSlits
|
||||
from csaxs_bec.bec_ipython_client.plugins.cSAXS.smaract import cSAXSInitSmaractStages
|
||||
from csaxs_bec.bec_ipython_client.plugins.cSAXS.smaract import cSAXSSmaract
|
||||
from csaxs_bec.bec_ipython_client.plugins.omny.omny_general_tools import OMNYTools
|
||||
from csaxs_bec.bec_ipython_client.plugins.cSAXS.filter_transmission import cSAXSFilterTransmission
|
||||
from csaxs_bec.bec_ipython_client.plugins.cSAXS.diagnostics import cSAXSDiagnostics
|
||||
from csaxs_bec.bec_ipython_client.plugins.cSAXS.slits import cSAXSSlits
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class cSAXSError(Exception):
|
||||
pass
|
||||
|
||||
@@ -36,6 +34,80 @@ class cSAXS(
|
||||
self.diagnostics = cSAXSDiagnostics()
|
||||
super().__init__(client=client)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Undulator
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def predict_gap(self, energy: float, n: int = 3) -> None:
|
||||
"""Print the predicted undulator gap for *energy* [keV] on harmonic *n*.
|
||||
|
||||
Examples
|
||||
--------
|
||||
csaxs.predict_gap(6.2) # h=3 (default)
|
||||
csaxs.predict_gap(10.0, n=5) # explicit harmonic
|
||||
"""
|
||||
import math
|
||||
|
||||
gap = float(_predict_gap(energy, n=n))
|
||||
if math.isnan(gap):
|
||||
print(f"Energy {energy:.3f} keV is unreachable on harmonic {n}.")
|
||||
else:
|
||||
print(f"Predicted gap for {energy:.3f} keV (h={n}): {gap:.4f} mm")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Help / discovery
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def commands(self) -> None:
|
||||
"""Print a table of all available cSAXS commands and sub-namespaces."""
|
||||
from rich import box
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
console = Console()
|
||||
|
||||
entries: list[tuple[str, str]] = []
|
||||
seen: set[str] = set()
|
||||
|
||||
for cls in type(self).__mro__:
|
||||
if cls is object:
|
||||
continue
|
||||
module = getattr(cls, "__module__", "") or ""
|
||||
if "csaxs_bec" not in module:
|
||||
continue
|
||||
for name, func in inspect.getmembers(cls, predicate=inspect.isfunction):
|
||||
if name.startswith("_") or name in seen:
|
||||
continue
|
||||
seen.add(name)
|
||||
doc = (inspect.getdoc(func) or "").split("\n")[0].strip()
|
||||
entries.append((name, doc))
|
||||
|
||||
entries.sort(key=lambda x: x[0])
|
||||
|
||||
tbl = Table(title="cSAXS Commands", box=box.SQUARE, show_lines=False)
|
||||
tbl.add_column("Command", style="cyan bold", no_wrap=True, min_width=46)
|
||||
tbl.add_column("Description")
|
||||
for name, doc in entries:
|
||||
tbl.add_row(f"csaxs.{name}()", doc)
|
||||
console.print(tbl)
|
||||
console.print("")
|
||||
|
||||
ns = Table(title="Sub-namespaces", box=box.SQUARE, show_lines=False)
|
||||
ns.add_column("Access", style="cyan bold", no_wrap=True, min_width=46)
|
||||
ns.add_column("Description")
|
||||
for access, desc in [
|
||||
("csaxs.diagnostics.show_all()", "All diagnostic device readbacks"),
|
||||
(
|
||||
"csaxs.diagnostics.bpm_xbox1 / .bpm_xbox2",
|
||||
"BPM diagnostics — .show_all(), .gain(val)",
|
||||
),
|
||||
("csaxs.diagnostics.bim", "BIM diagnostics — .show_all(), .gain(val)"),
|
||||
("csaxs.diagnostics.beamstop", "Beamstop diode — .show_all(), .gain(val)"),
|
||||
("csaxs.diagnostics.polarization", "Polarization diodes — .show_all(), .gain(val)"),
|
||||
("csaxs.OMNYTools.*", "OMNY instrument tools"),
|
||||
]:
|
||||
ns.add_row(access, desc)
|
||||
console.print(ns)
|
||||
|
||||
|
||||
# this is the csaxs master file that imports all routines from csaxs
|
||||
@@ -45,4 +117,6 @@ class cSAXS(
|
||||
# csaxs = cSAXS(bec)
|
||||
#
|
||||
# then all commands can be accessed by for example
|
||||
# csaxs._cSAXS_smaract_stages_.....
|
||||
# csaxs.commands()
|
||||
# csaxs.predict_gap(6.2)
|
||||
# csaxs._cSAXS_smaract_stages_...
|
||||
@@ -0,0 +1,26 @@
|
||||
"""Undulator gap predictor emitted by plot_intensity_map.py.
|
||||
Edit the fitted constants in the signature to retune."""
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
def predict_gap(energy, n=3, gap_min=5.0,
|
||||
E_inf=3.2878, c0=2.46086, c1=-0.468091, c2=0.0):
|
||||
"""Undulator gap [mm] to place `energy` [keV] on harmonic `n`.
|
||||
Fitted constants are the defaults below; edit them to retune.
|
||||
Returns NaN where the energy is unreachable on that harmonic."""
|
||||
energy = np.asarray(energy, float)
|
||||
arg = E_inf * n / energy - 1.0 # required K^2/2; must be > 0
|
||||
with np.errstate(invalid="ignore", divide="ignore"):
|
||||
y = np.log(arg)
|
||||
if abs(c2) < 1e-12:
|
||||
g = (y - c0) / c1
|
||||
else:
|
||||
disc = c1 * c1 - 4.0 * c2 * (c0 - y)
|
||||
sq = np.sqrt(np.where(disc >= 0, disc, np.nan))
|
||||
r1 = (-c1 + sq) / (2.0 * c2)
|
||||
r2 = (-c1 - sq) / (2.0 * c2)
|
||||
g = np.where(c1 + 2.0 * c2 * r1 < 0, r1, r2)
|
||||
g = np.where(arg > 0, g, np.nan) # above harmonic cutoff
|
||||
g = np.where(g >= gap_min, g, np.nan) # below mechanical minimum
|
||||
return g
|
||||
@@ -2560,6 +2560,10 @@ class Flomni(
|
||||
+ self.manual_shift_y
|
||||
)
|
||||
sum_offset_z = offsets[2]
|
||||
|
||||
# TODO this fix is while the tracker z is broken
|
||||
probe_propagation = -sum_offset_z * 1e-6
|
||||
sum_offset_z = 0
|
||||
|
||||
# --- positioning + laser tracker, mirroring
|
||||
# flomni_fermat_scan._prepare_setup_part2 ---
|
||||
@@ -2580,6 +2584,7 @@ class Flomni(
|
||||
# --- 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)
|
||||
self.tomo_reconstruct(probe_propagation=probe_propagation)
|
||||
|
||||
def _tomo_type1_actual_grid(self) -> tuple[int, float, int]:
|
||||
"""Compute the actual (achievable) tomo_type==1 grid from the
|
||||
@@ -2618,13 +2623,17 @@ class Flomni(
|
||||
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"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")
|
||||
# N, step, total_projections all come from the same helper
|
||||
# sub_tomo_scan() effectively uses internally - see
|
||||
# _tomo_type1_actual_grid() for why this can't just read
|
||||
# self.tomo_angle_stepsize directly.
|
||||
_, achievable_step, total_projections = self._tomo_type1_actual_grid()
|
||||
print(f"Total number of projections: {total_projections}")
|
||||
print(f"Angular step within sub-tomogram: {achievable_step:.3f} degrees")
|
||||
print(
|
||||
"Angular step of the final (combined) tomogram:"
|
||||
f" {self.tomo_angle_range / total_projections:.3f} degrees"
|
||||
|
||||
@@ -98,8 +98,9 @@ class FlomniOpticsMixin:
|
||||
dev.rtx.controller.feedback_disable()
|
||||
|
||||
self.fosa_out()
|
||||
foptx_out = self._get_user_param_safe("foptx", "out")
|
||||
fopty_out = self._get_user_param_safe("fopty", "out")
|
||||
umv(dev.fopty, fopty_out)
|
||||
umv(dev.foptx, foptx_out, dev.fopty, fopty_out)
|
||||
|
||||
if "rtx" in dev and dev.rtx.enabled:
|
||||
time.sleep(1)
|
||||
@@ -237,7 +238,7 @@ class FlomniOpticsMixin:
|
||||
|
||||
console.print(table)
|
||||
|
||||
diameters = [150e-6, 250e-6]
|
||||
diameters = [140e-6, 170e-6, 200e-6, 250e-6]
|
||||
|
||||
console = Console()
|
||||
table = Table(title="Outermost zone width \033[1m30 nm\033[0m", box=box.SQUARE)
|
||||
|
||||
@@ -70,14 +70,26 @@ class XrayEyeAlign:
|
||||
# so a second submission at step==1 is treated as the real angle-0
|
||||
# fit point instead of triggering another height correction.
|
||||
self._height_centered = False
|
||||
# Raw pixel coords + ROI size collected at each submit:
|
||||
# [[step_k, x_px, y_px, w_px, h_px, image_idx], ...]
|
||||
# image_idx refers to alignment_images[image_idx], i.e. the last
|
||||
# frame captured before that submit (shutter is closed at submit time).
|
||||
self.roi_pixel_data = []
|
||||
|
||||
def _save_alignment_data(self, file_path: str):
|
||||
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||
with h5py.File(os.path.expanduser(file_path), "w") as f:
|
||||
def _save_alignment_data(self, file_path: str, fit_data: np.ndarray | None = None):
|
||||
expanded = os.path.expanduser(file_path)
|
||||
os.makedirs(os.path.dirname(expanded), exist_ok=True)
|
||||
with h5py.File(expanded, "w") as f:
|
||||
f.create_dataset(
|
||||
"alignment_values", data=np.array(list(self.alignment_values.values()))
|
||||
)
|
||||
f.create_dataset("alignment_images", data=np.array(self.alignment_images))
|
||||
if self.roi_pixel_data:
|
||||
ds = f.create_dataset("roi_pixel_data", data=np.array(self.roi_pixel_data))
|
||||
ds.attrs["columns"] = ["step_k", "x_px", "y_px", "w_px", "h_px", "image_idx"]
|
||||
if fit_data is not None:
|
||||
ds = f.create_dataset("alignment_fit", data=fit_data)
|
||||
ds.attrs["rows"] = ["angles_deg", "offsets_um", "zeros"]
|
||||
|
||||
def update_frame(self, keep_shutter_open=False):
|
||||
if self.flomni._flomnigui_check_attribute_not_exists("xeyegui"):
|
||||
@@ -220,6 +232,20 @@ class XrayEyeAlign:
|
||||
# reset submit channel
|
||||
dev.omny_xray_gui.submit.set(0)
|
||||
|
||||
# Raw pixel position and ROI size at submit time.
|
||||
# The relevant image is the last captured frame (shutter is
|
||||
# closed by the time the user clicks submit).
|
||||
_raw_x = getattr(dev.omny_xray_gui, f"xval_x_{k}").get()
|
||||
_raw_y = getattr(dev.omny_xray_gui, f"yval_y_{k}").get()
|
||||
_raw_w = getattr(dev.omny_xray_gui, f"width_x_{k}").get()
|
||||
_raw_h = getattr(dev.omny_xray_gui, f"width_y_{k}").get()
|
||||
_img_idx = len(self.alignment_images) - 1
|
||||
print(
|
||||
f" Submit k={k}: px x={_raw_x:.1f} y={_raw_y:.1f} "
|
||||
f"w={_raw_w:.1f} h={_raw_h:.1f} img={_img_idx}"
|
||||
)
|
||||
self.roi_pixel_data.append([k, _raw_x, _raw_y, _raw_w, _raw_h, _img_idx])
|
||||
|
||||
# Controls whether `k` advances to the next step below. Left
|
||||
# True except for the height-centering submission, which
|
||||
# reuses k==1 for a second, real submission afterwards.
|
||||
@@ -252,9 +278,13 @@ class XrayEyeAlign:
|
||||
self.gui.show_crosshair()
|
||||
|
||||
self.send_message(
|
||||
"Adjust sample height with the arrows if needed, then mark "
|
||||
"the sample and submit - height will be centered automatically"
|
||||
)
|
||||
<<<<<<< Updated upstream
|
||||
"Submit height. Use arrows if far off."
|
||||
=======
|
||||
"Adjust sample height with the arrows if needed, then mark "
|
||||
"the sample and submit - height will be centered automatically"
|
||||
>>>>>>> Stashed changes
|
||||
)
|
||||
self.gui.enable_submit_button(True)
|
||||
self.movement_buttons_enabled(True, True)
|
||||
|
||||
@@ -373,37 +403,22 @@ class XrayEyeAlign:
|
||||
)
|
||||
|
||||
def write_output(self):
|
||||
file = os.path.expanduser("~/data/raw/logs/xrayeye_alignmentvalues/xrayeye_alignmentvalues")
|
||||
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
||||
self._save_alignment_data(file + f"_image_data_{timestamp}.h5")
|
||||
if not os.path.exists(file):
|
||||
os.makedirs(os.path.dirname(file), exist_ok=True)
|
||||
file_h5 = f"~/data/raw/logs/xrayeye_alignmentvalues/xrayeye_alignmentvalues_{timestamp}.h5"
|
||||
|
||||
with open(file, "w") as alignment_values_file:
|
||||
alignment_values_file.write("angle\thorizontal\n")
|
||||
fovx_offsets = np.zeros(5)
|
||||
for k in range(1, 6):
|
||||
fovx_offset = self.alignment_values[0] - self.alignment_values[k]
|
||||
fovx_offsets[k - 1] = fovx_offset
|
||||
print(f"Alignment number {k}, value x {fovx_offset}")
|
||||
|
||||
# Initialize an empty list to store fovx values
|
||||
fovx_list = []
|
||||
fovx_offsets = np.zeros(5) # holds offsets for k = 1..5
|
||||
|
||||
for k in range(1, 6):
|
||||
fovx_offset = self.alignment_values[0] - self.alignment_values[k]
|
||||
fovx_offsets[k - 1] = fovx_offset # store in array
|
||||
|
||||
fovx_x = (k - 1) * 45
|
||||
fovx_list.append([fovx_x, fovx_offset * 1000]) # Append the data to the list
|
||||
|
||||
print(f"Alignment number {k}, value x {fovx_offset}")
|
||||
alignment_values_file.write(f"{fovx_x}\t{fovx_offset * 1000}\n")
|
||||
|
||||
# Now build final numpy array:
|
||||
data = np.array(
|
||||
[
|
||||
[0, 45, 90, 135, 180], # angles
|
||||
fovx_offsets * 1000, # fovx_offset values
|
||||
[0, 0, 0, 0, 0],
|
||||
]
|
||||
)
|
||||
data = np.array(
|
||||
[
|
||||
[0, 45, 90, 135, 180], # angles
|
||||
fovx_offsets * 1000, # fovx_offset values
|
||||
[0, 0, 0, 0, 0],
|
||||
]
|
||||
)
|
||||
self._save_alignment_data(file_h5, fit_data=data)
|
||||
self.gui.submit_fit_array(data)
|
||||
print(f"fit submited with {data}")
|
||||
# self.flomni.flomnigui_show_xeyealign_fittab()
|
||||
print(f"fit submited with {data}")
|
||||
@@ -76,7 +76,10 @@ foptx:
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
#170 micron, 60 nm
|
||||
in: -13.831
|
||||
#in: -13.831
|
||||
#250 micron, 30 nm, Abe structures
|
||||
in: -13.8809375
|
||||
out: -14.1809
|
||||
fopty:
|
||||
description: Optics Y
|
||||
deviceClass: csaxs_bec.devices.omny.galil.fgalil_ophyd.FlomniGalilMotor
|
||||
@@ -95,8 +98,11 @@ fopty:
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
#170 micron, 60 nm
|
||||
in: 0.42
|
||||
out: 0.57
|
||||
#in: 0.42
|
||||
#out: 0.57
|
||||
#250 micron, 30 nm, Abe structures
|
||||
in: 2.8299
|
||||
out: 2.8299
|
||||
foptz:
|
||||
description: Optics Z
|
||||
deviceClass: csaxs_bec.devices.omny.galil.fgalil_ophyd.FlomniGalilMotor
|
||||
@@ -157,7 +163,7 @@ fsamy:
|
||||
host: mpc2844.psi.ch
|
||||
limits:
|
||||
- 2
|
||||
- 3.3
|
||||
- 3.8
|
||||
port: 8081
|
||||
sign: 1
|
||||
enabled: true
|
||||
@@ -305,7 +311,10 @@ fosax:
|
||||
#in: 8.7568
|
||||
#out: 5.1
|
||||
#170 micron, 60 nm, 7.9 kev
|
||||
in: 8.731922
|
||||
# in: 8.731922
|
||||
# out: 5.1
|
||||
#250 micron, 30 nm, Abe structures
|
||||
in: 8.755141
|
||||
out: 5.1
|
||||
fosay:
|
||||
description: OSA Y
|
||||
@@ -327,8 +336,9 @@ fosay:
|
||||
#170 micron, 60 nm, 7.6 kev
|
||||
#in: -0.0235
|
||||
#170 micron, 60 nm, 7.6 kev
|
||||
in: -0.0422
|
||||
|
||||
#in: -0.0422
|
||||
#250 micron, 30 nm, Abe structures
|
||||
in: -2.357436
|
||||
fosaz:
|
||||
description: OSA Z
|
||||
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
|
||||
@@ -350,8 +360,11 @@ fosaz:
|
||||
#in: 8.5
|
||||
#out: 6
|
||||
#170 micron, 60 nm, 7.9 kev, foptz 15.9
|
||||
in: 11.9
|
||||
out: 6
|
||||
# in: 11.9
|
||||
# out: 6
|
||||
# micron, 30 nm, 7.9 kev, foptz 32 //abe's fzp's
|
||||
in: -2
|
||||
out: -5
|
||||
|
||||
############################################################
|
||||
#################### flOMNI RT motors ######################
|
||||
|
||||
Reference in New Issue
Block a user