diff --git a/src/pymmcore_widgets/hcs/_plate_calibration_widget.py b/src/pymmcore_widgets/hcs/_plate_calibration_widget.py index 920683d10..008e80498 100644 --- a/src/pymmcore_widgets/hcs/_plate_calibration_widget.py +++ b/src/pymmcore_widgets/hcs/_plate_calibration_widget.py @@ -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 + # clear existing calibration widgets while self._calibration_widgets: wdg = self._calibration_widgets.popitem()[1] @@ -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] diff --git a/tests/hcs/test_well_calibration_widget.py b/tests/hcs/test_well_calibration_widget.py index c01418dd1..8c1a12eba 100644 --- a/tests/hcs/test_well_calibration_widget.py +++ b/tests/hcs/test_well_calibration_widget.py @@ -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( diff --git a/tests/hcs/test_well_plate_calibration_widget.py b/tests/hcs/test_well_plate_calibration_widget.py index bc198fad6..4f407af42 100644 --- a/tests/hcs/test_well_plate_calibration_widget.py +++ b/tests/hcs/test_well_plate_calibration_widget.py @@ -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)