Feat/flomni ontarget emitter #335
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}")
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user