Feat/flomni ontarget emitter #335

Merged
holler merged 3 commits from feat/flomni-ontarget-emitter into main 2026-10-20 15:19:34 +02:00
3 changed files with 205 additions and 0 deletions

No files matched your search

@@ -710,6 +710,82 @@ class FlomniSampleTransferMixin:
else:
print("The rt feedback is \x1b[91mNOT\x1b[0m running.")
def interferometer_trigger_show(self):
"""Show the detector-trigger stability criterion of the interferometer feedback.
A step-scan point is triggered when the relative position sample vs FZP (interferometer
minus FZP cap sensor) is within the tolerance in x and in y, each averaged over 0.5 ms,
for the hold time without interruption.
"""
values = dev.rtx.controller.emitter_get_ontarget()
tol_nm = values["ontarget_tolerance_um"] * 1000
if tol_nm > 0:
print(f"Detector trigger: |position error| x and y <= {tol_nm:.1f} nm "
f"for {values['ontarget_hold_ms']:.1f} ms.")
else:
print("Detector trigger: former criterion (on-target flags of the FZP PID loops).")
if values.get("trigger_on_move") is not None:
mode = "on" if values["trigger_on_move"] else "off"
print(f"Trigger after every position change (not only at scan points): {mode}.")
def interferometer_trigger_set_tolerance_nm(self, tolerance_nm: float):
"""Set the detector-trigger tolerance in nm (per axis, x and y).
Typical: 30 nm for a fast low-resolution scan, 10 nm standard, 2 nm for the highest
resolution (needs a few ms more per point). 0 selects the former criterion (on-target flags
of the FZP PID loops).
Args:
tolerance_nm (float): tolerance in nm, 0 or 1 ... 1000.
"""
if tolerance_nm != 0 and not 1 <= tolerance_nm <= 1000:
raise FlomniError(
"The trigger tolerance must be 0 (former criterion) or 1 ... 1000 nm."
)
dev.rtx.controller.emitter_set_ontarget_tolerance(tolerance_nm / 1000)
self.interferometer_trigger_show()
def interferometer_trigger_set_hold_ms(self, hold_ms: float):
"""Set how long (ms) the position must stay within the tolerance before the trigger.
Typical 3 ms. Longer = safer against a position that is only passing through the tolerance.
Args:
hold_ms (float): hold time in ms, 0 ... 100.
"""
if not 0 <= hold_ms <= 100:
raise FlomniError("The trigger hold time must be 0 ... 100 ms.")
dev.rtx.controller.emitter_set_ontarget_hold_ms(hold_ms)
self.interferometer_trigger_show()
def interferometer_trigger_set_after_move(self, enable: bool):
"""Send one detector trigger after every position change, not only at scan points.
With True every direct move of the sample stage (e.g. umv of rtx / rty / rtz, or
dev.rtx.controller.move_xyz) gives exactly one trigger, as soon as the position is stable
(same tolerance and hold time as for scan points). False: triggers at scan points only.
"""
dev.rtx.controller.emitter_set_trigger_on_move(1 if enable else 0)
self.interferometer_trigger_show()
def interferometer_trigger_help(self):
"""Print the detector-trigger commands with a short explanation."""
lines = [
"Detector trigger (step scans): a point is triggered when the relative position",
"sample vs FZP is within the tolerance in x and y (0.5 ms average) for the hold time.",
" flomni.interferometer_trigger_show() tolerance and hold time",
" flomni.interferometer_trigger_set_tolerance_nm(val) 30 coarse, 10 standard,",
" 2 finest; 0 = former",
" criterion (FZP PID flags)",
" flomni.interferometer_trigger_set_hold_ms(val) e.g. 3",
" flomni.interferometer_trigger_set_after_move(True) also one trigger after every",
" position change (False: off)",
" dev.rtx.controller.move_xyz(x, y, z) move all three axes at once",
"The values are live; after a restart of the control loop the start values",
"(10 nm, 3 ms) from the loop parameter file apply again.",
]
print("\n".join(lines))
def lights_off(self):
self.device_manager.devices.fsamx.controller.lights_off()
@@ -51,6 +51,12 @@ class RtFlomniController(Controller):
"emitter_set_tracking_target_z",
"emitter_set_intensity_threshold_laser",
"emitter_set_psd_intensity_threshold_tracking_low",
"emitter_get_ontarget",
"emitter_set_ontarget_tolerance",
"emitter_set_ontarget_hold_ms",
"emitter_help",
"emitter_set_trigger_on_move",
"move_xyz",
]
def __init__(
@@ -432,8 +438,102 @@ class RtFlomniController(Controller):
t.field_names = ["Name", "Value"]
for key, val in self.emitter_get().items():
t.add_row([key, val])
for key, val in self.emitter_get_ontarget().items():
t.add_row([key, val])
print(t)
def emitter_get_ontarget(self) -> dict:
"""Read the detector-trigger stability criterion (OnTargetTrigger in the control loop).
A step-scan point is triggered when the relative position sample vs FZP is within
``ontarget_tolerance_um`` in x and in y (each averaged over 0.5 ms) for ``ontarget_hold_ms``
without interruption.
Returns:
dict: ontarget_tolerance_um, ontarget_hold_ms, trigger_on_move (None from a server
that does not report it yet).
"""
ret = self.socket_put_and_receive("eg")
ret = ret.split("\n")[0]
values = [float(val) for val in ret.split(",")]
if len(values) not in (2, 3):
raise RtCommunicationError(
f"Expected to receive 2 or 3 on-target values. Instead received {values}"
)
return {
"ontarget_tolerance_um": values[0],
"ontarget_hold_ms": values[1],
"trigger_on_move": values[2] if len(values) == 3 else None,
}
def emitter_set_ontarget_tolerance(self, val_um: float) -> None:
"""Set the detector-trigger tolerance in um, per axis.
E.g. 0.030 for a low-resolution scan, 0.002 for the highest resolution. A value <= 0 selects
the former criterion (on-target flags of the FZP PID loops, set in their parameter files).
"""
self.socket_put(f"eo{val_um:.6f}")
def emitter_set_ontarget_hold_ms(self, val_ms: float) -> None:
"""Set the time in ms the position must stay within the tolerance before the trigger."""
self.socket_put(f"eh{val_ms:.3f}")
def emitter_set_trigger_on_move(self, val: int) -> None:
"""1: send one detector trigger after every direct position change (pa / p), once on target
(same criterion as a step-scan point); 0: triggers at step-scan points only."""
self.socket_put(f"em{1 if val else 0}")
def move_xyz(self, x: float, y: float, z: float) -> None:
"""Move rtx, rty and rtz with one command (CommunicationServer p<x>,<y>,<z>).
The three targets change in the same loop step, so with trigger_on_move there is exactly one
detector trigger for the move. Values as for the rtx / rty / rtz axes (axis sign applied).
Does not wait for the move.
"""
if self.laser_tracker_check_signalstrength() == "toolow":
raise RtError(
"The interferometer signal is too low for movements. Realignment required."
)
targets = []
for name, val in (("rtx", x), ("rty", y), ("rtz", z)):
axis = self.get_axis_by_name(name)
target_val = val * axis.sign
axis.user_setpoint.setpoint = target_val
targets.append(target_val)
self.socket_put(f"p{targets[0]:.4f},{targets[1]:.4f},{targets[2]:.4f}")
def emitter_help(self) -> None:
"""Print the ConstEmitter commands (loop runtime constants) with a short explanation."""
t = PrettyTable()
t.title = "ConstEmitter commands (rt controller)"
t.field_names = ["Method", "Wire", "Explanation"]
t.align = "l"
rows = [
("emitter_show_all()", "es, eg", "print all runtime constants"),
("emitter_get()", "es", "dict: tracking targets y/z, laser and PSD thresholds"),
("emitter_get_ontarget()", "eg", "dict: detector-trigger tolerance (um), hold (ms)"),
("emitter_set_tracking_target_y(val)", "ey", "target of the tracking loop y (PSD)"),
("emitter_set_tracking_target_z(val)", "ez", "target of the tracking loop z (PSD)"),
("emitter_set_intensity_threshold_laser(val)", "et",
"interferometer signal below this: slew limiters and x/y/z, FZP PIDs stop"),
("emitter_set_psd_intensity_threshold_tracking_low(val)", "el",
"PSD intensity below this: tracking PIDs y/z stop"),
("emitter_set_ontarget_tolerance(val_um)", "eo",
"detector trigger: |relative error| x and y <= val um (e.g. 0.030 coarse, 0.002 fine);"
" <= 0: FZP PID flags"),
("emitter_set_ontarget_hold_ms(val_ms)", "eh",
"detector trigger: ... held for val ms without interruption"),
("emitter_set_trigger_on_move(val)", "em",
"1: one detector trigger after every direct move (pa / p) once on target; 0: off"),
]
for row in rows:
t.add_row(row)
print(t)
print(
"Values are live in the control loop; the start values after a loop restart come from"
" ConstEmitter.xmp."
)
def emitter_set_tracking_target_y(self, val: float) -> None:
"""Set the y tracking target position (PID_tracking_y)."""
self.socket_put(f"ey{val:.4f}")
+29
View File
@@ -218,6 +218,27 @@ _There is also an enable without reset, which is used during tomography scans, w
- `flomni.feedback_disable()`
- `flomni.feedback_status()`
#### Detector trigger: position stability
In step scans the detector is triggered when the sample is stable at the scan point: the relative position
sample vs FZP (interferometer minus FZP cap sensor) must stay within a tolerance in x and in y (each averaged
over 0.5 ms) for a hold time. Both values are live and can be changed between scans:
- `flomni.interferometer_trigger_show()` — current tolerance and hold time
- `flomni.interferometer_trigger_set_tolerance_nm(10)` — tolerance per axis in nm: about 30 for a fast
low-resolution scan, 10 standard, 2 for the highest resolution (a few ms more per point);
0 selects the former criterion (on-target flags of the FZP loops)
- `flomni.interferometer_trigger_set_hold_ms(3)` — hold time in ms
- `flomni.interferometer_trigger_set_after_move(True)` — additionally one trigger after every position change
of the sample stage (e.g. `umv(dev.rtx, ...)`), as soon as the position is stable with the same tolerance and
hold time; `False` (default): triggers at scan points only
- `dev.rtx.controller.move_xyz(x, y, z)` — moves rtx, rty and rtz with one command (one trigger for the move)
- `flomni.interferometer_trigger_help()` — this list
After a restart of the control loop the start values (10 nm, 3 ms) apply again. Measured with 3 µm steps
(2026-10-20): the trigger comes about 36 ms (x) / 48 ms (y) after the step at 10 nm, 40 / 55 ms at 2 nm;
after the trigger the position stays within about 4 nm even at a 30 nm tolerance.
### Scanning in 2D and sample alignment
#### flOMNI Fermat scan
@@ -760,6 +781,14 @@ removed later.
[See short version](flomni.md#sample-storage-and-transfer)
### Detector trigger and interferometer tuning
The trigger tolerance and hold time: [Detector trigger: position stability](#detector-trigger-position-stability)
(`flomni.interferometer_trigger_*`). The control loops of the sample stage, the FZP scanner and the laser
tracker were characterized and retuned in October 2026; results, the present parameters and how to retune them
are documented in the flOMNI control repository (`FeedbackLoop/feedback_report.md` with PDF,
`FeedbackLoop/tuning_guide.md`).
### Heater
The [heater](https://www.dora.lib4ri.ch/psi/islandora/object/psi:45302) can be moved up and down by