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
54 changes: 44 additions & 10 deletions src/pymmcore_widgets/hcs/_plate_calibration_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,12 @@ def setValue(
self._current_plate = plate
self._plate_view.drawPlate(plate)

# Set minimum wells required based on plate size
if plate:
self._min_wells_required = min(3, plate.rows * plate.columns)
else:
self._min_wells_required = 3
Comment on lines +163 to +165
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a flexible min_wells_required sort of misses the point. Specifically: this determines whether we can calibrate for rotation (this value is used below in _origin_spacing_rotation). You always need 3 points for rotation, so a 2x1 doesn't just require 2 min_wells ... it still requires 3. This variable is ultimately more about "can/should we calibrate for rotation or not". It's always 3, or not possible

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤦‍♂️ you're right sorry...I'll find a better fix😀!


# clear existing calibration widgets
while self._calibration_widgets:
wdg = self._calibration_widgets.popitem()[1]
Expand Down Expand Up @@ -274,18 +280,46 @@ def _origin_spacing_rotation(
# not enough wells calibrated
return None

try:
params = well_coords_affine(self._calibrated_wells)
except ValueError:
# collinear points
if self._current_plate is None:
return None

a, b, ty, c, d, tx = params
unit_y = np.hypot(a, c) / 1000 # convert to mm
unit_x = np.hypot(b, d) / 1000 # convert to mm
rotation = round(np.rad2deg(np.arctan2(c, a)), 2)

return (round(tx, 4), round(ty, 4)), (unit_x, unit_y), rotation
num_calibrated = len(self._calibrated_wells)
if num_calibrated == 1:
# For single well, assume it's A1, use plate spacing, no rotation
center = next(iter(self._calibrated_wells.values()))
return center, self._current_plate.well_spacing, 0.0
elif num_calibrated == 2:
# For two wells, calculate spacing assuming they are adjacent, no rotation
indices = list(self._calibrated_wells.keys())
centers = list(self._calibrated_wells.values())
idx1, idx2 = indices
c1, c2 = centers
dr = abs(idx2[0] - idx1[0])
dc = abs(idx2[1] - idx1[1])
if dr == 0 and dc == 0:
return None # same well
dist = np.hypot(c2[0] - c1[0], c2[1] - c1[1])
spacing_val = dist / max(dr, dc) / 1000 # to mm
spacing = (spacing_val, spacing_val)
# Set A1 center to the calibrated well with smallest index
sorted_indices = sorted(self._calibrated_wells.keys())
a1_idx = sorted_indices[0]
a1_center = self._calibrated_wells[a1_idx]
return a1_center, spacing, 0.0
else:
# For 3 or more wells, use full affine transformation
try:
params = well_coords_affine(self._calibrated_wells)
except ValueError:
# collinear points
return None

a, b, ty, c, d, tx = params
unit_y = np.hypot(a, c) / 1000 # convert to mm
unit_x = np.hypot(b, d) / 1000 # convert to mm
rotation = round(np.rad2deg(np.arctan2(c, a)), 2)

return (round(tx, 4), round(ty, 4)), (unit_x, unit_y), rotation

def _get_or_create_well_calibration_widget(
self, idx: tuple[int, int]
Expand Down
6 changes: 4 additions & 2 deletions tests/hcs/test_well_calibration_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,15 @@ def test_well_calibration_widget_modes(
combo = wdg._calibration_mode_wdg
modes = [Mode(*combo.itemData(i, COMBO_ROLE)) for i in range(combo.count())]
# make sure the modes are correct
assert modes == MODES[circular]
expected = [(mode.text, mode.points) for mode in MODES[circular]]
actual = [(m[0], m[1]) for m in modes]
assert actual == expected
# make sure that the correct number of rows are displayed when the mode is changed
for idx, mode in enumerate(modes):
# set the mode
combo.setCurrentIndex(idx)
# get the number of rows
assert wdg._table.rowCount() == mode.points
assert wdg._table.rowCount() == mode[1]


def test_well_calibration_widget_positions(
Expand Down
31 changes: 31 additions & 0 deletions tests/hcs/test_well_plate_calibration_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,3 +204,34 @@ def test_plate_calibration_test_positions(global_mmcore: CMMCorePlus, qtbot) ->
data.append((hover_item_data.x, hover_item_data.y, hover_item_data.name))

assert data == expected_data


def test_small_plate_calibration(qtbot) -> None:
"""Test calibration for plates with fewer than 3 wells."""
wdg = PlateCalibrationWidget()
qtbot.addWidget(wdg)

# Test 1x1 plate
plate_1x1 = useq.WellPlate(
rows=1, columns=1, well_size=(5, 5), well_spacing=(10, 10)
)
wdg.setValue(plate_1x1)
assert wdg._min_wells_required == 1

# Simulate calibrating the single well
wdg._calibrated_wells = {(0, 0): (100.0, 200.0)}
result = wdg._origin_spacing_rotation()
assert result == ((100.0, 200.0), (10, 10), 0.0)

# Test 1x2 plate
plate_1x2 = useq.WellPlate(
rows=1, columns=2, well_size=(5, 5), well_spacing=(10, 10)
)
wdg.setValue(plate_1x2)
assert wdg._min_wells_required == 2

# Simulate calibrating two wells
wdg._calibrated_wells = {(0, 0): (100.0, 200.0), (0, 1): (10100.0, 200.0)}
result = wdg._origin_spacing_rotation()
# Spacing should be calculated as distance / dc = 10000 / 1 / 1000 = 10 mm
assert result == ((100.0, 200.0), (10.0, 10.0), 0.0)
Loading