diff --git a/pylabrobot/plate_reading/tecan/infinite_backend.py b/pylabrobot/plate_reading/tecan/infinite_backend.py index 1c6423d089b..992e422c342 100644 --- a/pylabrobot/plate_reading/tecan/infinite_backend.py +++ b/pylabrobot/plate_reading/tecan/infinite_backend.py @@ -586,6 +586,10 @@ async def _run_scan( _, y_stage = self._map_well_to_stage(row_wells[0]) await self._send_command(f"ABSOLUTE MTP,Y={y_stage}") + # Match the OEM one-row scan flow by explicitly pre-positioning the transport to the + # row start before issuing SCANX. Hardware testing showed the standalone XY move alone + # can reintroduce the first-row edge-read problem. + await self._send_command(f"ABSOLUTE MTP,X={start_x},Y={y_stage}") await self._send_command(f"SCAN DIRECTION={scan_direction}") await self._send_command( f"SCANX {start_x},{end_x},{count}", wait_for_terminal=False, read_response=False diff --git a/pylabrobot/plate_reading/tecan/infinite_backend_tests.py b/pylabrobot/plate_reading/tecan/infinite_backend_tests.py index 1deb0ccaf8a..59e3f797bec 100644 --- a/pylabrobot/plate_reading/tecan/infinite_backend_tests.py +++ b/pylabrobot/plate_reading/tecan/infinite_backend_tests.py @@ -733,9 +733,11 @@ async def mock_await(decoder, row_count, mode): call(self._frame("PREPARE REF")), # row scans (2 rows in test plate) call(self._frame("ABSOLUTE MTP,Y=8000")), + call(self._frame("ABSOLUTE MTP,X=3000,Y=8000")), call(self._frame("SCAN DIRECTION=ALTUP")), call(self._frame("SCANX 3000,23000,3")), call(self._frame("ABSOLUTE MTP,Y=16000")), + call(self._frame("ABSOLUTE MTP,X=23000,Y=16000")), call(self._frame("SCAN DIRECTION=ALTUP")), call(self._frame("SCANX 23000,3000,3")), # _end_run @@ -769,6 +771,41 @@ async def mock_terminal(_saw_terminal): self.assertAlmostEqual(result[0]["data"][0][0], 0.3010299956639812) + async def test_read_absorbance_subset_prepositions_to_masked_row_start(self): + self.backend._ready = True + wells = self.plate.get_wells(["A2", "A3", "B1", "B2"]) + + async def mock_await(decoder, row_count, mode): + cal_len, cal_blob = _abs_calibration_blob(6000, 0, 1000, 0, 1000) + if decoder.calibration is None: + decoder.feed_bin(cal_len, cal_blob) + for _ in range(row_count): + data_len, data_blob = _abs_data_blob(6000, 500, 1000) + decoder.feed_bin(data_len, data_blob) + + with patch.object(self.backend, "_await_measurements", side_effect=mock_await): + with patch.object(self.backend, "_await_scan_terminal", new_callable=AsyncMock): + result = await self.backend.read_absorbance(self.plate, wells, wavelength=600) + + self.mock_usb.write.assert_has_calls( + [ + call(self._frame("ABSOLUTE MTP,Y=8000")), + call(self._frame("ABSOLUTE MTP,X=13000,Y=8000")), + call(self._frame("SCAN DIRECTION=ALTUP")), + call(self._frame("SCANX 13000,23000,2")), + call(self._frame("ABSOLUTE MTP,Y=16000")), + call(self._frame("ABSOLUTE MTP,X=13000,Y=16000")), + call(self._frame("SCAN DIRECTION=ALTUP")), + call(self._frame("SCANX 13000,3000,2")), + ] + ) + self.assertIsNone(result[0]["data"][0][0]) + self.assertAlmostEqual(result[0]["data"][0][1], 0.3010299956639812) + self.assertAlmostEqual(result[0]["data"][0][2], 0.3010299956639812) + self.assertAlmostEqual(result[0]["data"][1][0], 0.3010299956639812) + self.assertAlmostEqual(result[0]["data"][1][1], 0.3010299956639812) + self.assertIsNone(result[0]["data"][1][2]) + async def test_read_fluorescence_commands(self): """Test that read_fluorescence sends the correct configuration commands.""" self.backend._ready = True @@ -827,9 +864,11 @@ async def mock_await(decoder, row_count, mode): call(self._frame("PREPARE REF")), # row scans (2 rows in test plate) call(self._frame("ABSOLUTE MTP,Y=8000")), + call(self._frame("ABSOLUTE MTP,X=3000,Y=8000")), call(self._frame("SCAN DIRECTION=UP")), call(self._frame("SCANX 3000,23000,3")), call(self._frame("ABSOLUTE MTP,Y=16000")), + call(self._frame("ABSOLUTE MTP,X=23000,Y=16000")), call(self._frame("SCAN DIRECTION=UP")), call(self._frame("SCANX 23000,3000,3")), # _end_run @@ -886,9 +925,11 @@ async def mock_await(decoder, row_count, mode): call(self._frame("PREPARE REF")), # row scans (2 rows, non-serpentine so both scan left-to-right) call(self._frame("ABSOLUTE MTP,Y=8000")), + call(self._frame("ABSOLUTE MTP,X=3000,Y=8000")), call(self._frame("SCAN DIRECTION=UP")), call(self._frame("SCANX 3000,23000,3")), call(self._frame("ABSOLUTE MTP,Y=16000")), + call(self._frame("ABSOLUTE MTP,X=3000,Y=16000")), call(self._frame("SCAN DIRECTION=UP")), call(self._frame("SCANX 3000,23000,3")), # _end_run