Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions jungfrau_gui/globals.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,6 @@ def get_git_info():

default_HT = 200000.00 # V
backlash = [100, 80, 0, 0]

# overshoot/preload used for two-step jog moves
preload = [2000, 2000, 0, 1] # X,Y,Z in nm (2 µm), TX in deg (1°)
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def run(self):

if tilt_X_abs < 5:
if np.abs(movexy[0]) < self.thresholds['dxy_min'] and np.abs(movexy[1]) < self.thresholds['dxy_min']:
logging.info(f'Vector already small enough (< {self.thresholds['dxy_min']} um): {movexy[0]}, {movexy[1]}')
logging.info(f"Vector already small enough (< {self.thresholds['dxy_min']} um): {movexy[0]}, {movexy[1]}")
return
logging.info(f'Move X: {movexy[0]} um, Y: {movexy[1]} um with MAG: {magnification[2]}')
self.control.trigger_movewithbacklash.emit(np.sign(movexy[0]) > 0, -movexy[0]*globals.UM_TO_NM, globals.backlash[0], False)
Expand Down
181 changes: 179 additions & 2 deletions jungfrau_gui/ui_components/tem_controls/task/task_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@

from ..gaussian_fitter_mp import GaussianFitterMP

from jungfrau_gui.ui_components.tem_controls.task.tem_dispatcher import TEMDispatcher

def on_new_best_result_in_main_thread(result_dict):
# This runs in the main thread. We can safely update GUI elements, logs, etc.
print("New best result =>", result_dict)
Expand Down Expand Up @@ -56,6 +58,7 @@ class ControlWorker(QObject):
trigger_getteminfo = Signal(str)
trigger_centering = Signal(bool, str)
trigger_movewithbacklash = Signal(int, float, float, bool)
trigger_move_parking = Signal(int, float, float, bool) # always preload

actionFit_Beam = Signal() # originally defined with QuGui
# actionAdjustZ = Signal()
Expand All @@ -64,6 +67,7 @@ def __init__(self, tem_action): #, timeout:int=10, buffer=1024):
super().__init__()
self.cfg = ConfigurationClient(redis_host(), token=auth_token())
self.client = TEMClient(globals.tem_host, 3535, verbose=False)
self.tem = TEMDispatcher(self.client)

self.task = Task(self, "Dummy")
self.task_thread = QThread()
Expand All @@ -85,6 +89,10 @@ def __init__(self, tem_action): #, timeout:int=10, buffer=1024):
self.trigger_getteminfo.connect(self.getteminfo)
self.trigger_centering.connect(self.centering)
self.trigger_movewithbacklash.connect(self.move_with_backlash)
self.trigger_move_parking.connect(self.move_parking_with_preload)

self._net_away = {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0} # X,Y,Z,TX signed net move

# self.actionAdjustZ.connect(self.start_adjustZ)

self.beam_fitter = None
Expand Down Expand Up @@ -749,6 +757,10 @@ def stop_task(self):
def shutdown(self):
logging.info("Shutting down control")
try:
try:
self.tem.shutdown()
except Exception:
pass
# self.client.exit_server()
# logging.warning("TEM server is OFF")
# time.sleep(0.12)
Expand All @@ -758,6 +770,8 @@ def shutdown(self):
logging.error(f'Shutdown of Task Manager triggered error: {e}')
pass

# KT's implementation of backlash corrected fast movements
# The routine moves the stage in one direction, but with a reduced/corrected value
@Slot(int, float, float, bool)
def move_with_backlash(self, moverid=0, value=10, backlash=0, button=False, scale=1):
"""
Expand Down Expand Up @@ -786,7 +800,7 @@ def move_with_backlash(self, moverid=0, value=10, backlash=0, button=False, scal
elif direction == 1 and np.sign(self.tem_status["stage.GetPos_diff"][axis]) < 0:
backlash = 0

logging.debug(f"xyz0, dxyz0 : {list(map(lambda x, y: f'{x/globals.UM_TO_NM:8.3f}{y/globals.UM_TO_NM:8.3f}', self.tem_status['stage.GetPos'][:3], self.tem_status['stage.GetPos_diff'][:3]))}, "
logging.debug(f"xyz0, dxyz0 : {list(map(lambda x, y: f'{x/1e3:8.3f}{y/1e3:8.3f}', self.tem_status['stage.GetPos'][:3], self.tem_status['stage.GetPos_diff'][:3]))}, "
f"{self.tem_status['stage.GetPos'][3]:6.2f} {self.tem_status['stage.GetPos_diff'][3]:6.2f}, {backlash}"
)

Expand Down Expand Up @@ -819,10 +833,173 @@ def move_with_backlash(self, moverid=0, value=10, backlash=0, button=False, scal
return

if moverid < 2 and button: # display the previous move to user
# logging.info(f"Moved stage {value*scale/1e3:.1f} um in X-direction")
if moverid == 0:
self.tem_action.tem_stagectrl.movex10ump.setStyleSheet('background-color: rgb(53, 53, 53); color: rgb(128, 128, 255);')
self.tem_action.tem_stagectrl.movex10umn.setStyleSheet('background-color: rgb(53, 53, 53); color: white;')
else:
self.tem_action.tem_stagectrl.movex10ump.setStyleSheet('background-color: rgb(53, 53, 53); color: white;')
self.tem_action.tem_stagectrl.movex10umn.setStyleSheet('background-color: rgb(53, 53, 53); color: rgb(128, 128, 255);')
logging.debug(f"xyz1, dxyz1 : {list(map(lambda x, y: f'{x/globals.UM_TO_NM:8.3f}{y/globals.UM_TO_NM:8.3f}', self.tem_status['stage.GetPos'][:3], self.tem_status['stage.GetPos_diff'][:3]))}, {self.tem_status['stage.GetPos'][3]:6.2f} {self.tem_status['stage.GetPos_diff'][3]:6.2f}, {backlash}")
logging.debug(f"xyz1, dxyz1 : {list(map(lambda x, y: f'{x/1e3:8.3f}{y/1e3:8.3f}', self.tem_status['stage.GetPos'][:3], self.tem_status['stage.GetPos_diff'][:3]))}, {self.tem_status['stage.GetPos'][3]:6.2f} {self.tem_status['stage.GetPos_diff'][3]:6.2f}, {backlash}")


# ************************************************
# Routines for lag-corrected fast movement actions
# ************************************************

def _SetTXRel_timeout(self, val: float, timeout_ms: int):
# Calls underlying TEMClient with custom timeout, without modifying TEMClient.
return self.client._send_message("SetTXRel", val, timeout_ms=timeout_ms)

def _SetXRel_timeout(self, val: float, timeout_ms: int):
return self.client._send_message("SetXRel", val, timeout_ms=timeout_ms)

def _SetYRel_timeout(self, val: float, timeout_ms: int):
return self.client._send_message("SetYRel", val, timeout_ms=timeout_ms)

def _SetZRel_timeout(self, val: float, timeout_ms: int):
return self.client._send_message("SetZRel", val, timeout_ms=timeout_ms)


def _two_step_sequence(self, axis_fn, value, preload, settle_s=0.1):
"""
Build an atomic sequence for preload-compensated moves:
value > 0: (value+preload), wait, (-preload)
value < 0: (value-preload), wait, (+preload)
"""
if value == 0:
return []
if preload == 0:
return [(axis_fn, (value,), {})]

if value > 0:
return [
(axis_fn, (value + preload,), {}),
(time.sleep, (settle_s,), {}),
(axis_fn, (-preload,), {}),
]
else:
return [
(axis_fn, (value - preload,), {}),
(time.sleep, (settle_s,), {}),
(axis_fn, (preload,), {}),
]


@Slot(int, float, float, bool)
def move_parking_with_preload(self, moverid=0, value=10.0, preload=0.0, button=False, scale=1.0):
"""
Relative stage jog with optional backlash (preload) compensation.

If a direction change is detected, applies a two-step move to take up slack:
+dir: (value + preload) then (-preload)
-dir: (value - preload) then (+preload)
Otherwise, sends a single relative move.

Args:
moverid (int): 0/1:+X/-X, 2/3:+Y/-Y, 4/5:+Z/-Z, 6/7:+TX/-TX.
value (float): Relative move (nm for X/Y/Z, deg for TX); sign matches moverid.
preload (float): Overshoot amount (same units as value); 0 disables.
button (bool): If True, updates X jog button highlight.
scale (float): Multiplier applied to `value` before sending.
"""
# Ask for fresh status (coalesced, won't spam)
QTimer.singleShot(0, lambda: self.send_to_tem("#info", asynchronous=True))

axis = moverid // 2
movement_value = value * scale
effective_preload = float(preload) #if (apply_preload and preload != 0) else 0.0

# set longer timeout for slow rotation speeds
MOVE_TIMEOUT_MS = 30000 if axis == 3 else 5000

# pick axis function
axis_fn = self._axis_fn_for_axis(axis, MOVE_TIMEOUT_MS)

# Build atomic job sequence and enqueue on single TEM lane
jobs = self._two_step_sequence(axis_fn, movement_value, effective_preload, settle_s=0.05)
if jobs:
self.tem.post_sequence(jobs)

self._record_away_and_enable_back(axis, movement_value)

# UI styling for X buttons
if moverid < 2 and button:
if moverid == 0:
self.tem_action.tem_stagectrl.movex10ump.setStyleSheet(
"background-color: rgb(53, 53, 53); color: rgb(128, 128, 255);"
)
self.tem_action.tem_stagectrl.movex10umn.setStyleSheet(
"background-color: rgb(53, 53, 53); color: white;"
)
else:
self.tem_action.tem_stagectrl.movex10ump.setStyleSheet(
"background-color: rgb(53, 53, 53); color: white;"
)
self.tem_action.tem_stagectrl.movex10umn.setStyleSheet(
"background-color: rgb(53, 53, 53); color: rgb(128, 128, 255);"
)


@Slot(int)
def move_back_no_preload(self, axis=0):
"""
Move back by the last recorded away move on this axis.
axis: 0=X,1=Y,2=Z,3=TX
"""
away_accumulated = float(self._net_away.get(axis, 0.0))
if away_accumulated == 0.0:
logging.warning(f"No recorded away move for axis {axis}")
self._clear_away_and_disable(axis)
return

back_value = -away_accumulated
MOVE_TIMEOUT_MS = 30000 if axis == 3 else 5000
axis_fn = self._axis_fn_for_axis(axis, MOVE_TIMEOUT_MS)

self.tem.post_sequence([(axis_fn, (back_value,), {})])

self._clear_away_and_disable(axis)

def _axis_fn_for_axis(self, axis: int, timeout_ms: int):
# Helper to choose corresponding TEMClient routine as fct of the set axis
if axis == 0:
return lambda v: self._SetXRel_timeout(v, timeout_ms)
if axis == 1:
return lambda v: self._SetYRel_timeout(v, timeout_ms)
if axis == 2:
return lambda v: self._SetZRel_timeout(v, timeout_ms)
if axis == 3:
return lambda v: self._SetTXRel_timeout(v, timeout_ms)
raise ValueError(f"Invalid axis {axis}")


def _record_away_and_enable_back(self, axis, movement_value):
# record the net “away” move so 'Back' buttons know what to do
self._net_away[axis] += movement_value

# enable back button corresponding to the latest preloaded fast movement
if axis == 0: # translation (X)
QTimer.singleShot(0, lambda: self.tem_action.tem_stagectrl.back_x.setEnabled(True))
if axis == 3: # rotation (TX)
QTimer.singleShot(0, lambda: self.tem_action.tem_stagectrl.back_tx.setEnabled(True))


def _clear_away_and_disable(self, axis):
# Clear 'away' as compensation has been completed
self._net_away[axis] = 0.0

# Disable back buttons
if axis == 0: # translation (X)
QTimer.singleShot(0, lambda: self.tem_action.tem_stagectrl.back_x.setEnabled(False))
if axis == 3: # rotation (TX)
QTimer.singleShot(0, lambda: self.tem_action.tem_stagectrl.back_tx.setEnabled(False))

# Uncolor translation buttons
if axis == 0:
self.tem_action.tem_stagectrl.movex10ump.setStyleSheet(
"background-color: rgb(53, 53, 53); color: white;"
)
self.tem_action.tem_stagectrl.movex10umn.setStyleSheet(
"background-color: rgb(53, 53, 53); color: white;"
)
77 changes: 77 additions & 0 deletions jungfrau_gui/ui_components/tem_controls/task/tem_dispatcher.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import queue
import threading
import time
import logging

class TEMDispatcher:
"""
Single-threaded command lane for TEM calls.
- post(fn, ...): fire-and-forget
- call(fn, ...): wait for result
- post_sequence([...]): run multiple commands atomically (no interleave)
- post_latest(key, fn, ...): keep only the newest request for a key (ideal for polling)
"""
def __init__(self, client):
self.client = client
self._q = queue.Queue()
self._stop = threading.Event()
self._latest_token = {} # key -> token
self._thread = threading.Thread(target=self._loop, name="TEM-IO", daemon=True)
self._thread.start()

def shutdown(self):
self._stop.set()
self._q.put(None)

def _loop(self):
while not self._stop.is_set():
item = self._q.get()
if item is None:
break

kind = item[0]
try:
if kind == "call":
_, fn, args, kwargs, done, out = item
out["result"] = fn(*args, **kwargs)
done.set()

elif kind == "post":
_, fn, args, kwargs = item
fn(*args, **kwargs)

elif kind == "sequence":
_, fns = item
for fn, args, kwargs in fns:
fn(*args, **kwargs)

elif kind == "latest":
_, key, token, fn, args, kwargs = item
# skip stale polls
if self._latest_token.get(key) == token:
fn(*args, **kwargs)

except Exception as e:
logging.warning(f"TEMDispatcher error in {kind}: {type(e).__name__}: {e}")

def post(self, fn, *args, **kwargs):
self._q.put(("post", fn, args, kwargs))

def call(self, fn, *args, timeout=None, **kwargs):
done = threading.Event()
out = {}
self._q.put(("call", fn, args, kwargs, done, out))
ok = done.wait(timeout)
return out.get("result") if ok else None

def post_sequence(self, fns):
"""
fns = [(fn, args_tuple, kwargs_dict), ...]
executed back-to-back in the TEM thread.
"""
self._q.put(("sequence", fns))

def post_latest(self, key, fn, *args, **kwargs):
token = object()
self._latest_token[key] = token
self._q.put(("latest", key, token, fn, args, kwargs))
35 changes: 27 additions & 8 deletions jungfrau_gui/ui_components/tem_controls/tem_action.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,17 +88,36 @@ def __init__(self, parent, grandparent):

self.control.updated.connect(self.on_tem_update)

# Move X positive 10 micrometers
self.tem_stagectrl.movex10ump.clicked.connect(lambda: self.control.trigger_movewithbacklash.emit(0, 10000, globals.backlash[0], True))
# Move X negative 10 micrometers
self.tem_stagectrl.movex10umn.clicked.connect(lambda: self.control.trigger_movewithbacklash.emit(1, -10000, globals.backlash[0], True))
# Move TX positive 10 degrees
self.tem_stagectrl.move10degp.clicked.connect(lambda: self.control.trigger_movewithbacklash.emit(6, 10, globals.backlash[3], False))
# Move TX negative 10 degrees
self.tem_stagectrl.move10degn.clicked.connect(lambda: self.control.trigger_movewithbacklash.emit(7, -10, globals.backlash[3], False))
# Away X: +10 um, always preload (e.g. +12 then -2)
self.tem_stagectrl.movex10ump.clicked.connect(
lambda: self.control.trigger_move_parking.emit(0, 10000, globals.preload[0], True)
)
self.tem_stagectrl.movex10umn.clicked.connect(
lambda: self.control.trigger_move_parking.emit(1, -10000, globals.preload[0], True)
)

# Away TX: +10 deg, preload (e.g. +11 then -1)
self.tem_stagectrl.move10degp.clicked.connect(
lambda: self.control.trigger_move_parking.emit(6, 10, globals.preload[3], False)
)
self.tem_stagectrl.move10degn.clicked.connect(
lambda: self.control.trigger_move_parking.emit(7, -10, globals.preload[3], False)
)

# Stage translation move back (X=0) with no preload
self.tem_stagectrl.back_x.clicked.connect(
lambda: self.control.move_back_no_preload(0)
)

# Stage rotation move back (TX=3) with no preload
self.tem_stagectrl.back_tx.clicked.connect(
lambda: self.control.move_back_no_preload(3)
)

# Set Tilt X Angle to 0 degrees
self.tem_stagectrl.move0deg.clicked.connect(
lambda: threading.Thread(target=self.control.client.SetTiltXAngle, args=(0,)).start())

self.tem_stagectrl.go_button.clicked.connect(self.go_listedposition)
self.tem_stagectrl.addpos_button.clicked.connect(lambda: self.add_listedposition())
self.trigger_additem.connect(self.add_listedposition)
Expand Down
Loading