Skip to content

Commit 018075f

Browse files
authored
Merge pull request #130 from AllenNeuralDynamics/wip/ephys-link
Wip/ephys link Approved from reviewers
2 parents dfa5571 + 032125a commit 018075f

12 files changed

+555
-285
lines changed

parallax/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import os
66

7-
__version__ = "1.3.1"
7+
__version__ = "1.4.0"
88

99
# allow multiple OpenMP instances
1010
os.environ["KMP_DUPLICATE_LIB_OK"] = "True"

parallax/calculator.py

Lines changed: 63 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@
1111
from PyQt5.uic import loadUi
1212
from PyQt5.QtCore import Qt
1313

14+
from .coords_converter import CoordsConverter
15+
from .stage_controller import StageController
16+
1417
logger = logging.getLogger(__name__)
15-
logger.setLevel(logging.DEBUG)
18+
logger.setLevel(logging.WARNING)
1619

1720
package_dir = os.path.dirname(os.path.abspath(__file__))
1821
debug_dir = os.path.join(os.path.dirname(package_dir), "debug")
@@ -26,7 +29,7 @@ class Calculator(QWidget):
2629
reticle adjustments, and issuing movement commands to stages.
2730
"""
2831

29-
def __init__(self, model, reticle_selector, stage_controller):
32+
def __init__(self, model, reticle_selector):
3033
"""
3134
Initializes the Calculator widget by setting up the UI components and connecting relevant signals.
3235
@@ -37,9 +40,10 @@ def __init__(self, model, reticle_selector, stage_controller):
3740
"""
3841
super().__init__()
3942
self.model = model
43+
self.stage_controller = StageController(self.model)
44+
self.coords_converter = CoordsConverter(self.model)
4045
self.reticle_selector = reticle_selector
4146
self.reticle = None
42-
self.stage_controller = stage_controller
4347

4448
self.ui = loadUi(os.path.join(ui_dir, "calc.ui"), self)
4549
self.setWindowTitle("Calculator")
@@ -113,17 +117,17 @@ def set_calc_functions(self):
113117
"""
114118
for stage_sn, item in self.model.transforms.items():
115119
transM, scale = item[0], item[1]
116-
if transM is not None: # Set calc function for calibrated stages
120+
if transM is not None and scale is not None: # Set calc function for calibrated stages
117121
push_button = self.findChild(QPushButton, f"convert_{stage_sn}")
118122
if not push_button:
119123
logger.warning(f"Error: QPushButton for {stage_sn} not found")
120124
continue
121125
self._enable(stage_sn)
122-
push_button.clicked.connect(self._create_convert_function(stage_sn, transM, scale))
126+
push_button.clicked.connect(self._create_convert_function(stage_sn))
123127
else: # Block calc functions for uncalibrated stages
124128
self._disable(stage_sn)
125129

126-
def _create_convert_function(self, stage_sn, transM, scale):
130+
def _create_convert_function(self, stage_sn):
127131
"""
128132
Creates a lambda function for converting coordinates for a specific stage.
129133
@@ -137,11 +141,9 @@ def _create_convert_function(self, stage_sn, transM, scale):
137141
"""
138142
logger.debug("\n=== Creating convert function ===")
139143
logger.debug(f"Stage SN: {stage_sn}")
140-
logger.debug(f"transM: {transM}")
141-
logger.debug(f"scale: {scale}")
142-
return lambda: self._convert(stage_sn, transM, scale)
144+
return lambda: self._convert(stage_sn)
143145

144-
def _convert(self, sn, transM, scale):
146+
def _convert(self, sn):
145147
"""
146148
Performs the conversion between local and global coordinates based on the user's input.
147149
Depending on the entered values, the function converts global to local or local to global coordinates.
@@ -160,15 +162,18 @@ def _convert(self, sn, transM, scale):
160162
localZ = self.findChild(QLineEdit, f"localZ_{sn}").text()
161163

162164
logger.debug("- Convert -")
163-
logger.debug(f"Global: {globalX}, {globalY}, {globalZ}")
164-
logger.debug(f"Local: {localX}, {localY}, {localZ}")
165+
logger.debug(f"User Input (Global): {globalX}, {globalY}, {globalZ}")
166+
logger.debug(f"User Input (Local): {localX}, {localY}, {localZ}")
167+
logger.debug(f"User Input (Local): {self.reticle}")
165168
trans_type, local_pts, global_pts = self._get_transform_type(globalX, globalY, globalZ, localX, localY, localZ)
166169
if trans_type == "global_to_local":
167-
local_pts_ret = self._apply_inverse_transformation(global_pts, transM, scale)
168-
self._show_local_pts_result(sn, local_pts_ret)
170+
local_pts_ret = self.coords_converter.global_to_local(sn, global_pts, self.reticle)
171+
if local_pts_ret is not None:
172+
self._show_local_pts_result(sn, local_pts_ret)
169173
elif trans_type == "local_to_global":
170-
global_pts_ret = self._apply_transformation(local_pts, transM, scale)
171-
self._show_global_pts_result(sn, global_pts_ret)
174+
global_pts_ret = self.coords_converter.local_to_global(sn, local_pts, self.reticle)
175+
if global_pts_ret is not None:
176+
self._show_global_pts_result(sn, global_pts_ret)
172177
else:
173178
logger.warning(f"Error: Invalid transforsmation type for {sn}")
174179
return
@@ -253,117 +258,6 @@ def is_valid_number(s):
253258
else:
254259
return None, None, None
255260

256-
def _apply_reticle_adjustments(self, global_pts):
257-
"""
258-
Applies the selected reticle's adjustments (rotation and offsets) to the given global coordinates.
259-
260-
Args:
261-
global_pts (ndarray): The global coordinates to adjust.
262-
263-
Returns:
264-
tuple: The adjusted global coordinates (x, y, z).
265-
"""
266-
reticle_metadata = self.model.get_reticle_metadata(self.reticle)
267-
reticle_rot = reticle_metadata.get("rot", 0)
268-
reticle_rotmat = reticle_metadata.get("rotmat", np.eye(3)) # Default to identity matrix if not found
269-
reticle_offset = np.array([
270-
reticle_metadata.get("offset_x", global_pts[0]),
271-
reticle_metadata.get("offset_y", global_pts[1]),
272-
reticle_metadata.get("offset_z", global_pts[2])
273-
])
274-
275-
if reticle_rot != 0:
276-
# Transpose because points are row vectors
277-
global_pts = global_pts @ reticle_rotmat.T
278-
global_pts = global_pts + reticle_offset
279-
280-
global_x = np.round(global_pts[0], 1)
281-
global_y = np.round(global_pts[1], 1)
282-
global_z = np.round(global_pts[2], 1)
283-
return global_x, global_y, global_z
284-
285-
def _apply_transformation(self, local_point_, transM_LR, scale):
286-
"""
287-
Applies the transformation to convert local coordinates to global coordinates.
288-
289-
Args:
290-
local_point (ndarray): The local coordinates to be transformed.
291-
transM_LR (ndarray): The transformation matrix.
292-
scale (ndarray): The scale factors for the coordinates.
293-
294-
Returns:
295-
ndarray: The transformed global coordinates.
296-
"""
297-
local_point = local_point_ * scale
298-
local_point = np.append(local_point, 1)
299-
global_point = np.dot(transM_LR, local_point)
300-
logger.debug(f"local_to_global: {local_point_} -> {global_point[:3]}")
301-
logger.debug(f"R: {transM_LR[:3, :3]}\nT: {transM_LR[:3, 3]}")
302-
303-
# Ensure the reticle is defined and get its metadata
304-
if self.reticle and self.reticle != "Global coords":
305-
# Apply the reticle offset and rotation adjustment
306-
global_x, global_y, global_z = self._apply_reticle_adjustments(global_point[:3])
307-
# Return the adjusted global coordinates
308-
return np.array([global_x, global_y, global_z])
309-
310-
return global_point[:3]
311-
312-
def _apply_reticle_adjustments_inverse(self, global_point):
313-
"""
314-
Applies the inverse of reticle adjustments to the global coordinates.
315-
316-
Args:
317-
global_point (ndarray): The global coordinates to adjust.
318-
319-
Returns:
320-
ndarray: The adjusted global coordinates.
321-
"""
322-
if self.reticle and self.reticle != "Global coords":
323-
# Convert global_point to numpy array if it's not already
324-
global_point = np.array(global_point)
325-
326-
# Get the reticle metadata
327-
reticle_metadata = self.model.get_reticle_metadata(self.reticle)
328-
329-
# Get rotation matrix (default to identity if not found)
330-
reticle_rotmat = reticle_metadata.get("rotmat", np.eye(3))
331-
332-
# Get offset values, default to global point coordinates if not found
333-
reticle_offset = np.array([
334-
reticle_metadata.get("offset_x", 0), # Default to 0 if no offset is provided
335-
reticle_metadata.get("offset_y", 0),
336-
reticle_metadata.get("offset_z", 0)
337-
])
338-
339-
# Subtract the reticle offset
340-
global_point = global_point - reticle_offset
341-
# Undo the rotation
342-
global_point = np.dot(global_point, reticle_rotmat)
343-
344-
return global_point
345-
346-
def _apply_inverse_transformation(self, global_point, transM_LR, scale):
347-
"""
348-
Applies the inverse transformation to convert global coordinates to local coordinates.
349-
350-
Args:
351-
global_point (ndarray): The global coordinates.
352-
transM_LR (ndarray): The transformation matrix.
353-
scale (ndarray): The scale factors for the coordinates.
354-
355-
Returns:
356-
ndarray: The transformed local coordinates.
357-
"""
358-
global_point = self._apply_reticle_adjustments_inverse(global_point)
359-
360-
# Transpose the 3x3 rotation part
361-
R_T = transM_LR[:3, :3].T
362-
local_point = np.dot(R_T, global_point - transM_LR[:3, 3])
363-
logger.debug(f"global_to_local {global_point} -> {local_point / scale}")
364-
logger.debug(f"R.T: {R_T}\nT: {transM_LR[:3, 3]}")
365-
return local_point / scale
366-
367261
def _disable(self, sn):
368262
"""
369263
Disables the group box and clears the input fields for the given stage.
@@ -442,7 +336,7 @@ def _connect_move_stage_buttons(self):
442336
for stage_sn in self.model.stages.keys():
443337
moveXY_button = self.findChild(QPushButton, f"moveStageXY_{stage_sn}")
444338
if moveXY_button:
445-
moveXY_button.clicked.connect(self._create_stage_function(stage_sn, "moveXY"))
339+
moveXY_button.clicked.connect(self._create_stage_function(stage_sn))
446340

447341
def _stop_stage(self, move_type):
448342
"""
@@ -455,9 +349,9 @@ def _stop_stage(self, move_type):
455349
command = {
456350
"move_type": move_type
457351
}
458-
self.stage_controller.stop_request(command)
352+
self.stage_controller.request(command)
459353

460-
def _create_stage_function(self, stage_sn, move_type):
354+
def _create_stage_function(self, stage_sn):
461355
"""
462356
Creates a function to move the stage to specified coordinates.
463357
@@ -468,9 +362,9 @@ def _create_stage_function(self, stage_sn, move_type):
468362
Returns:
469363
function: A lambda function to move the stage.
470364
"""
471-
return lambda: self._move_stage(stage_sn, move_type)
365+
return lambda: self._move_stage(stage_sn)
472366

473-
def _move_stage(self, stage_sn, move_type):
367+
def _move_stage(self, stage_sn):
474368
"""
475369
Moves the stage to the coordinates specified in the input fields, with confirmation and safety checks.
476370
@@ -480,6 +374,7 @@ def _move_stage(self, stage_sn, move_type):
480374
"""
481375
try:
482376
# Convert the text to float, round it, then cast to int
377+
# Move request is in mm, so divide by 1000
483378
x = float(self.findChild(QLineEdit, f"localX_{stage_sn}").text()) / 1000
484379
y = float(self.findChild(QLineEdit, f"localY_{stage_sn}").text()) / 1000
485380
z = 15.0 # Z is inverted in the server.
@@ -493,20 +388,27 @@ def _move_stage(self, stage_sn, move_type):
493388
return
494389

495390
# Use the confirm_move_stage function to ask for confirmation
496-
if self._confirm_move_stage(x, y):
497-
# If the user confirms, proceed with moving the stage
498-
print(f"Moving stage {stage_sn} to ({np.round(x*1000)}, {np.round(y*1000)}, 0)")
499-
command = {
500-
"stage_sn": stage_sn,
501-
"move_type": move_type,
502-
"x": x,
503-
"y": y,
504-
"z": z
505-
}
506-
self.stage_controller.move_request(command)
507-
else:
508-
# If the user cancels, do nothing
391+
if not self._confirm_move_stage(x, y):
509392
print("Stage move canceled by user.")
393+
return # User canceled the move
394+
395+
# If the user confirms, proceed with moving the stage
396+
command = {
397+
"stage_sn": stage_sn,
398+
"move_type": "stepMode",
399+
"stepMode": 0 # 0 for coarse, 1 for fine
400+
}
401+
self.stage_controller.request(command)
402+
403+
command = {
404+
"stage_sn": stage_sn,
405+
"move_type": "moveXY0",
406+
"x": x,
407+
"y": y,
408+
"z": z
409+
}
410+
self.stage_controller.request(command)
411+
print(f"Moving stage {stage_sn} to ({np.round(x*1000)}, {np.round(y*1000)}, 0)")
510412

511413
def _is_z_safe_pos(self, stage_sn, x, y, z):
512414
"""
@@ -524,23 +426,25 @@ def _is_z_safe_pos(self, stage_sn, x, y, z):
524426
# Z is inverted in the server
525427
local_pts_z15 = [float(x) * 1000, float(y) * 1000, float(15.0 - z) * 1000] # Should be top of the stage
526428
local_pts_z0 = [float(x) * 1000, float(y) * 1000, 15.0 * 1000] # Should be bottom
527-
for sn, item in self.model.transforms.items():
429+
for sn, _ in self.model.transforms.items():
528430
if sn != stage_sn:
529431
continue
530432

531-
transM, scale = item[0], item[1]
532-
if transM is not None:
533-
try:
534-
# Apply transformations to get global points for Z=15 and Z=0
535-
global_pts_z15 = self._apply_transformation(local_pts_z15, transM, scale)
536-
global_pts_z0 = self._apply_transformation(local_pts_z0, transM, scale)
537-
538-
# Ensure that Z=15 is higher than Z=0 and Z=15 is positive
539-
if global_pts_z15[2] > global_pts_z0[2] and global_pts_z15[2] > 0:
540-
return True
541-
except Exception as e:
542-
logger.error(f"Error applying transformation for stage {stage_sn}: {e}")
543-
return False
433+
try:
434+
# Apply transformations to get global points for Z=15 and Z=0
435+
global_pts_z15 = self.coords_converter.local_to_global(stage_sn, local_pts_z15)
436+
global_pts_z0 = self.coords_converter.local_to_global(stage_sn, local_pts_z0)
437+
438+
if global_pts_z15 is None or global_pts_z0 is None:
439+
return False # Transformation failed, return False
440+
441+
# Ensure that Z=15 is higher than Z=0 and Z=15 is positive
442+
if global_pts_z15[2] > global_pts_z0[2] and global_pts_z15[2] > 0:
443+
return True
444+
445+
except Exception as e:
446+
logger.error(f"Error applying transformation for stage {stage_sn}: {e}")
447+
return False
544448
return False
545449

546450
def _confirm_move_stage(self, x, y):

0 commit comments

Comments
 (0)