{ "cells": [ { "cell_type": "markdown", "id": "ed97a2cc", "metadata": {}, "source": [ "# SpectraMax Gemini EM\n", "\n", "The Molecular Devices SpectraMax Gemini EM is a fluorescence plate reader controlled over a\n", "serial RS-232 interface. PyLabRobot supports this reader with the\n", "{class}`~pylabrobot.plate_reading.MolecularDevicesSpectraMaxGeminiEMBackend`.\n", "\n", "## Installation\n", "\n", "```bash\n", "pip install pylabrobot[serial]\n", "```\n", "\n", "On Linux, the USB-to-RS-232 adapter will typically appear as `/dev/ttyUSB0` or `/dev/ttyUSB1`.\n", "On Windows, it appears as a `COM` port." ] }, { "cell_type": "code", "execution_count": 7, "id": "6742e81a", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "2026-05-17 13:10:52,216 - pylabrobot.io.serial - INFO - Using explicitly provided port: /dev/ttyUSB0 (for VID=None, PID=None)\n" ] } ], "source": [ "from pylabrobot.plate_reading import PlateReader, MolecularDevicesSpectraMaxGeminiEMBackend\n", "\n", "backend = MolecularDevicesSpectraMaxGeminiEMBackend(port=\"/dev/ttyUSB0\")\n", "pr = PlateReader(name=\"gemini\", backend=backend, size_x=0, size_y=0, size_z=0)\n", "\n", "await pr.setup()" ] }, { "cell_type": "markdown", "id": "2d64c8b7-a946-4178-a1be-e0c97dea5cc4", "metadata": {}, "source": [ "### Open Tray" ] }, { "cell_type": "code", "execution_count": 8, "id": "429c5df1", "metadata": {}, "outputs": [], "source": [ "await pr.open()" ] }, { "cell_type": "markdown", "id": "e71af884-ac5d-477f-ba4d-bbf7151b2ca6", "metadata": {}, "source": [ "### Close Tray" ] }, { "cell_type": "code", "execution_count": 9, "id": "72250570", "metadata": {}, "outputs": [], "source": [ "await pr.close()" ] }, { "cell_type": "markdown", "id": "90e68eea", "metadata": {}, "source": [ "\n", "## Validated behavior\n", "\n", "The Gemini EM backend was validated against a Molecular Devices SpectraMax Gemini EM using\n", "a USB-to-RS-232 adapter. The instrument identified itself as:\n", "\n", "```text\n", "GEMINI EM\n", "2.00b78 01Mar04\n", "```\n", "\n", "The following operations were validated:\n", "\n", "- opening and closing the drawer\n", "- querying reader status\n", "- querying and setting temperature\n", "- endpoint fluorescence reads on a 96-well plate\n", "- endpoint fluorescence reads on a 384-well plate\n", "- endpoint luminescence reads on a 96-well plate\n", "- endpoint time-resolved fluorescence reads on a 96-well plate\n", "- kinetic time-resolved fluorescence reads on a 96-well plate\n", "- emission and excitation fluorescence spectrum reads on a 96-well plate\n", "- fluorescence fill wellscan reads on a 96-well plate\n", "- top and bottom fluorescence read-stage selection\n", "- configurable excitation and emission wavelengths\n", "\n", "The Windows/SoftMax Pro setup used during development was for protocol discovery only. The intended\n", "deployment path is direct serial control from Linux using the USB-to-RS-232 adapter, typically as\n", "`/dev/ttyUSB0` or `/dev/ttyUSB1`.\n", "\n", "Absorbance and fluorescence polarization are not implemented for this backend.\n", "\n", "## Support status\n", "\n", "| Mode | Backend status | Hardware status |\n", "| --- | --- | --- |\n", "| Drawer open/close | implemented | validated |\n", "| Status and temperature query | implemented | validated |\n", "| Temperature setpoint | implemented | validated |\n", "| Fluorescence endpoint | implemented | validated, 96-well and 384-well |\n", "| Fluorescence top/bottom read | implemented | validated |\n", "| Luminescence endpoint | implemented | validated, top read |\n", "| Time-resolved fluorescence endpoint | implemented | validated |\n", "| Time-resolved fluorescence kinetic | implemented | validated with a short run |\n", "| Fluorescence emission spectrum | implemented | validated with a short sweep |\n", "| Fluorescence excitation spectrum | implemented | validated with a short sweep |\n", "| Fluorescence wellscan | implemented | validated with fill pattern |\n", "| Rectangular partial-region reads | implemented | unit tested |\n", "| Arbitrary non-rectangular partial reads | not implemented | not validated |\n", "| Absorbance | not supported by Gemini EM | not applicable |\n", "| Fluorescence polarization | not implemented | not validated |\n", "\n", "## Fluorescence reads\n", "\n", "Assign a plate to the reader before reading. The backend uses the plate resource geometry to\n", "configure the reader's plate position." ] }, { "cell_type": "code", "execution_count": 3, "id": "e20d3b47", "metadata": {}, "outputs": [], "source": [ "from pylabrobot.resources import Cor_96_wellplate_360ul_Fb\n", "\n", "plate = Cor_96_wellplate_360ul_Fb(name=\"plate\")\n", "pr.assign_child_resource(plate)\n", "\n", "data = await backend.read_fluorescence(\n", " plate=plate,\n", " wells=plate.get_all_items(),\n", " excitation_wavelength=485,\n", " emission_wavelength=520,\n", ")" ] }, { "cell_type": "markdown", "id": "ab6f037d", "metadata": {}, "source": [ "\n", "For a TRITC-like fluorescence read:" ] }, { "cell_type": "code", "execution_count": 3, "id": "8bbc6209", "metadata": {}, "outputs": [], "source": [ "data = await backend.read_fluorescence(\n", " plate=plate,\n", " wells=plate.get_all_items(),\n", " excitation_wavelength=557,\n", " emission_wavelength=576,\n", ")" ] }, { "cell_type": "markdown", "id": "422cfe65", "metadata": {}, "source": [ "\n", "For a bottom read, pass `read_from_bottom=True`:" ] }, { "cell_type": "code", "execution_count": 4, "id": "d947905e", "metadata": {}, "outputs": [], "source": [ "data = await backend.read_fluorescence(\n", " plate=plate,\n", " wells=plate.get_all_items(),\n", " excitation_wavelength=485,\n", " emission_wavelength=520,\n", " read_from_bottom=True,\n", ")" ] }, { "cell_type": "markdown", "id": "ec5d69d5", "metadata": {}, "source": [ "\n", "Full-plate reads and rectangular contiguous well regions are supported. Arbitrary non-rectangular\n", "well selections raise `NotImplementedError`.\n", "\n", "## Luminescence reads\n", "\n", "Endpoint luminescence is available through the Gemini EM backend. The implementation follows the\n", "SoftMax Pro command sequence captured during development, including `!READTYPE LUM`,\n", "`!EMWAVELENGTH 0`, `!TOPREADCLEAR OFF`, and `!READSTAGE TOP`." ] }, { "cell_type": "code", "execution_count": 5, "id": "69a2f9ca", "metadata": {}, "outputs": [], "source": [ "data = await backend.read_luminescence(\n", " plate=plate,\n", " wells=plate.get_all_items(),\n", ")" ] }, { "cell_type": "markdown", "id": "fed7b43e", "metadata": {}, "source": [ "\n", "Optional pre-read shaking is supported:" ] }, { "cell_type": "code", "execution_count": 6, "id": "2a5ce1f0", "metadata": {}, "outputs": [], "source": [ "from pylabrobot.plate_reading.molecular_devices import ShakeSettings\n", "\n", "data = await backend.read_luminescence(\n", " plate=plate,\n", " wells=plate.get_all_items(),\n", " shake_settings=ShakeSettings(before_read=True, before_read_duration=10),\n", ")" ] }, { "cell_type": "markdown", "id": "c4c6ee47", "metadata": {}, "source": [ "\n", "Only endpoint, top-read luminescence is currently implemented.\n", "\n", "## Time-Resolved Fluorescence\n", "\n", "Endpoint time-resolved fluorescence is available through the Gemini EM backend. The implementation\n", "uses the Gemini EM command form observed from SoftMax Pro:\n", "\n", "```text\n", "!READTYPE TIME \n", "```\n", "\n", "For example:" ] }, { "cell_type": "code", "execution_count": 7, "id": "283533e0", "metadata": {}, "outputs": [], "source": [ "data = await backend.experimental_read_time_resolved_fluorescence(\n", " plate=plate,\n", " excitation_wavelengths=[485],\n", " emission_wavelengths=[525],\n", " cutoff_filters=[7],\n", " delay_time=50,\n", " integration_time=850,\n", ")" ] }, { "cell_type": "markdown", "id": "eb1efe84", "metadata": {}, "source": [ "\n", "For a bottom TRF read:" ] }, { "cell_type": "code", "execution_count": 8, "id": "c0fa5761", "metadata": {}, "outputs": [], "source": [ "data = await backend.experimental_read_time_resolved_fluorescence(\n", " plate=plate,\n", " excitation_wavelengths=[485],\n", " emission_wavelengths=[525],\n", " cutoff_filters=[7],\n", " delay_time=50,\n", " integration_time=850,\n", " read_from_bottom=True,\n", ")" ] }, { "cell_type": "markdown", "id": "e694a570", "metadata": {}, "source": [ "\n", "Kinetic TRF is also supported with `ReadType.KINETIC` and `KineticSettings`:" ] }, { "cell_type": "code", "execution_count": 9, "id": "05c7613a", "metadata": {}, "outputs": [], "source": [ "from pylabrobot.plate_reading.molecular_devices import KineticSettings, ReadType\n", "\n", "data = await backend.experimental_read_time_resolved_fluorescence(\n", " plate=plate,\n", " excitation_wavelengths=[485],\n", " emission_wavelengths=[525],\n", " cutoff_filters=[7],\n", " delay_time=50,\n", " integration_time=850,\n", " read_type=ReadType.KINETIC,\n", " kinetic_settings=KineticSettings(interval=30, num_readings=21),\n", ")" ] }, { "cell_type": "markdown", "id": "fee71830", "metadata": {}, "source": [ "\n", "Endpoint and kinetic TRF are currently implemented for full plates and rectangular contiguous well\n", "regions. Arbitrary non-rectangular well selections are not supported.\n", "\n", "## Fluorescence Spectra\n", "\n", "Gemini-specific helpers are available for fluorescence spectra. They use `SpectrumSettings`\n", "internally but expose fixed-excitation and fixed-emission sweeps directly.\n", "\n", "For an emission spectrum with fixed excitation:" ] }, { "cell_type": "code", "execution_count": 10, "id": "bb22c2f5", "metadata": {}, "outputs": [ { "ename": "CancelledError", "evalue": "", "output_type": "error", "traceback": [ "\u001b[31m---------------------------------------------------------------------------\u001b[39m", "\u001b[31mCancelledError\u001b[39m Traceback (most recent call last)", "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[10]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m data = await backend.experimental_read_fluorescence_emission_spectrum(\n\u001b[32m 2\u001b[39m plate=plate,\n\u001b[32m 3\u001b[39m wells=plate.get_all_items(),\n\u001b[32m 4\u001b[39m excitation_wavelength=\u001b[32m350\u001b[39m,\n", "\u001b[36mFile \u001b[39m\u001b[32m~/git/pylabrobot/pylabrobot/plate_reading/molecular_devices/spectramax_gemini_em_backend.py:406\u001b[39m, in \u001b[36mMolecularDevicesSpectraMaxGeminiEMBackend.experimental_read_fluorescence_emission_spectrum\u001b[39m\u001b[34m(self, plate, wells, excitation_wavelength, start_emission_wavelength, step, num_steps, cutoff_filters, read_order, calibrate, shake_settings, carriage_speed, read_from_bottom, pmt_gain, flashes_per_well, timeout)\u001b[39m\n\u001b[32m 403\u001b[39m \u001b[38;5;28;01mawait\u001b[39;00m \u001b[38;5;28mself\u001b[39m._set_order(settings)\n\u001b[32m 405\u001b[39m \u001b[38;5;28;01mawait\u001b[39;00m \u001b[38;5;28mself\u001b[39m._read_now()\n\u001b[32m--> \u001b[39m\u001b[32m406\u001b[39m \u001b[38;5;28;01mawait\u001b[39;00m \u001b[38;5;28mself\u001b[39m._wait_for_idle(timeout=timeout)\n\u001b[32m 407\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;01mawait\u001b[39;00m \u001b[38;5;28mself\u001b[39m._transfer_data(settings)\n", "\u001b[36mFile \u001b[39m\u001b[32m~/git/pylabrobot/pylabrobot/plate_reading/molecular_devices/backend.py:689\u001b[39m, in \u001b[36mMolecularDevicesBackend._wait_for_idle\u001b[39m\u001b[34m(self, timeout)\u001b[39m\n\u001b[32m 687\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m time.time() - start_time > timeout:\n\u001b[32m 688\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mTimeoutError\u001b[39;00m(\u001b[33m\"\u001b[39m\u001b[33mTimeout waiting for plate reader to become idle.\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m--> \u001b[39m\u001b[32m689\u001b[39m status = \u001b[38;5;28;01mawait\u001b[39;00m \u001b[38;5;28mself\u001b[39m.get_status()\n\u001b[32m 690\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m status \u001b[38;5;129;01mand\u001b[39;00m status[\u001b[32m1\u001b[39m] == \u001b[33m\"\u001b[39m\u001b[33mIDLE\u001b[39m\u001b[33m\"\u001b[39m:\n\u001b[32m 691\u001b[39m \u001b[38;5;28;01mbreak\u001b[39;00m\n", "\u001b[36mFile \u001b[39m\u001b[32m~/git/pylabrobot/pylabrobot/plate_reading/molecular_devices/backend.py:333\u001b[39m, in \u001b[36mMolecularDevicesBackend.get_status\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 332\u001b[39m \u001b[38;5;28;01masync\u001b[39;00m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mget_status\u001b[39m(\u001b[38;5;28mself\u001b[39m) -> List[\u001b[38;5;28mstr\u001b[39m]:\n\u001b[32m--> \u001b[39m\u001b[32m333\u001b[39m res = \u001b[38;5;28;01mawait\u001b[39;00m \u001b[38;5;28mself\u001b[39m.send_command(\u001b[33m\"\u001b[39m\u001b[33m!STATUS\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m 334\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(res) > \u001b[32m1\u001b[39m:\n\u001b[32m 335\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m res[\u001b[32m1\u001b[39m].split()\n", "\u001b[36mFile \u001b[39m\u001b[32m~/git/pylabrobot/pylabrobot/plate_reading/molecular_devices/backend.py:287\u001b[39m, in \u001b[36mMolecularDevicesBackend.send_command\u001b[39m\u001b[34m(self, command, timeout, num_res_fields)\u001b[39m\n\u001b[32m 285\u001b[39m timeout_time = time.time() + timeout\n\u001b[32m 286\u001b[39m \u001b[38;5;28;01mwhile\u001b[39;00m \u001b[38;5;28;01mTrue\u001b[39;00m:\n\u001b[32m--> \u001b[39m\u001b[32m287\u001b[39m raw_response += \u001b[38;5;28;01mawait\u001b[39;00m \u001b[38;5;28mself\u001b[39m.io.readline()\n\u001b[32m 288\u001b[39m \u001b[38;5;28;01mawait\u001b[39;00m asyncio.sleep(\u001b[32m0.001\u001b[39m)\n\u001b[32m 289\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m time.time() > timeout_time:\n", "\u001b[36mFile \u001b[39m\u001b[32m~/git/pylabrobot/pylabrobot/io/serial.py:244\u001b[39m, in \u001b[36mSerial.readline\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 241\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m._executor \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m._ser \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m 242\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mRuntimeError\u001b[39;00m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mCall setup() first for device \u001b[39m\u001b[33m'\u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m._human_readable_device_name\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m'\u001b[39m\u001b[33m.\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m--> \u001b[39m\u001b[32m244\u001b[39m data = \u001b[38;5;28;01mawait\u001b[39;00m loop.run_in_executor(\u001b[38;5;28mself\u001b[39m._executor, \u001b[38;5;28mself\u001b[39m._ser.readline)\n\u001b[32m 246\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(data) != \u001b[32m0\u001b[39m:\n\u001b[32m 247\u001b[39m logger.log(LOG_LEVEL_IO, \u001b[33m\"\u001b[39m\u001b[33m[\u001b[39m\u001b[38;5;132;01m%s\u001b[39;00m\u001b[33m] readline \u001b[39m\u001b[38;5;132;01m%s\u001b[39;00m\u001b[33m\"\u001b[39m, \u001b[38;5;28mself\u001b[39m._port, data)\n", "\u001b[31mCancelledError\u001b[39m: " ] } ], "source": [ "data = await backend.experimental_read_fluorescence_emission_spectrum(\n", " plate=plate,\n", " wells=plate.get_all_items(),\n", " excitation_wavelength=350,\n", " start_emission_wavelength=400,\n", " step=10,\n", " num_steps=36,\n", " read_from_bottom=True,\n", ")" ] }, { "cell_type": "markdown", "id": "eb84d2e5", "metadata": {}, "source": [ "\n", "This sends the Gemini EM spectrum mode:\n", "\n", "```text\n", "!EXWAVELENGTH 350\n", "!MODE EMSPECTRUM 400 10 36\n", "!ORDER WAVELENGTH\n", "```\n", "\n", "For an excitation spectrum with fixed emission:" ] }, { "cell_type": "code", "execution_count": null, "id": "1dc20c10", "metadata": {}, "outputs": [], "source": [ "data = await backend.experimental_read_fluorescence_excitation_spectrum(\n", " plate=plate,\n", " wells=plate.get_all_items(),\n", " emission_wavelength=600,\n", " start_excitation_wavelength=350,\n", " step=20,\n", " num_steps=4,\n", " read_from_bottom=True,\n", ")" ] }, { "cell_type": "markdown", "id": "2e68b915", "metadata": {}, "source": [ "\n", "This sends:\n", "\n", "```text\n", "!EMWAVELENGTH 600\n", "!AUTOFILTER EX OFF\n", "!MODE EXSPECTRUM 350 20 4\n", "!ORDER WAVELENGTH\n", "```\n", "\n", "The spectrum helpers default to cutoff filter `1`, matching the captured SoftMax Pro spectrum\n", "protocols. Pass `cutoff_filters=[...]` to override this.\n", "\n", "## Wellscan fluorescence\n", "\n", "The Gemini EM backend includes a Gemini-specific `experimental_read_fluorescence_wellscan` method. SoftMax\n", "Pro implements wellscan by enabling wellscan mode and performing separate reads at shifted plate\n", "origins. The backend mirrors that behavior instead of averaging scan points silently." ] }, { "cell_type": "code", "execution_count": null, "id": "be68e7a3", "metadata": {}, "outputs": [], "source": [ "data = await backend.experimental_read_fluorescence_wellscan(\n", " plate=plate,\n", " wells=plate.get_all_items(),\n", " excitation_wavelength=485,\n", " emission_wavelength=525,\n", " pattern=\"fill\",\n", ")" ] }, { "cell_type": "markdown", "id": "c5c8bbeb", "metadata": {}, "source": [ "\n", "Supported patterns are:\n", "\n", "```text\n", "horizontal\n", "vertical\n", "cross\n", "fill\n", "```\n", "\n", "Each returned read includes `wellscan_point`, `wellscan_x`, and `wellscan_y` fields.\n", "\n", "## 384-well plates\n", "\n", "384-well fluorescence reads were validated with `Greiner_384_wellplate_28ul_Fb`:" ] }, { "cell_type": "code", "execution_count": null, "id": "f6894ce7", "metadata": {}, "outputs": [], "source": [ "from pylabrobot.resources import Greiner_384_wellplate_28ul_Fb\n", "\n", "plate = Greiner_384_wellplate_28ul_Fb(name=\"plate\")\n", "pr.assign_child_resource(plate)\n", "\n", "data = await backend.read_fluorescence(\n", " plate=plate,\n", " wells=plate.get_all_items(),\n", " excitation_wavelength=557,\n", " emission_wavelength=576,\n", ")" ] }, { "cell_type": "markdown", "id": "982a6bdc", "metadata": {}, "source": [ "\n", "## OEM command observations\n", "\n", "SoftMax Pro was used during development to observe the OEM serial behavior. The observed\n", "bottom-read setup sequence was:\n", "\n", "```text\n", "!OPTION\n", "!TEMP\n", "!CLEAR DATA\n", "!TAG OFF\n", "!WELLSCANMODE\n", "!XPOS 14.380 9 12\n", "!YPOS 11.235 9 8\n", "!SHAKE OFF\n", "!SHAKE 0 0 0 0 0\n", "!STRIP 1 12\n", "!READTYPE FLU\n", "!EMWAVELENGTH 525\n", "!AUTOFILTER OFF\n", "!EMFILTER 7\n", "!EXWAVELENGTH 490\n", "!FPW 6\n", "!TOPREADCLEAR ON\n", "!AUTOPMT ON\n", "!CSPEED 8\n", "!PMTCAL ON\n", "!MODE ENDPOINT\n", "!ORDER COLUMN\n", "!READSTAGE BOT\n", "!READ\n", "```\n", "\n", "The Gemini EM responds to `!READSTAGE BOT` and `!READSTAGE TOP` with a single `OK` response\n", "field. This differs from some other Molecular Devices readers and is handled by the Gemini EM\n", "backend.\n", "\n", "SoftMax Pro sent `!TOPREADCLEAR ON` before selecting either read stage. The Gemini EM backend\n", "does the same before sending `!READSTAGE TOP` or `!READSTAGE BOT`.\n", "\n", "### Luminescence\n", "\n", "SoftMax Pro used this sequence for a luminescence endpoint read with a 10 second pre-read shake:\n", "\n", "```text\n", "!CLEAR DATA\n", "!TAG OFF\n", "!WELLSCANMODE\n", "!XPOS 14.380 9 12\n", "!YPOS 11.235 9 8\n", "!SHAKE ON\n", "!SHAKE 10 0 0 0 0\n", "!STRIP 1 12\n", "!READTYPE LUM\n", "!EMWAVELENGTH 0\n", "!FPW 6\n", "!TOPREADCLEAR OFF\n", "!AUTOPMT ON\n", "!CSPEED 8\n", "!PMTCAL ON\n", "!MODE ENDPOINT\n", "!ORDER COLUMN\n", "!READSTAGE TOP\n", "!READ\n", "```\n", "\n", "### Time-resolved fluorescence\n", "\n", "SoftMax Pro used this sequence for a TRF endpoint read:\n", "\n", "```text\n", "!TAG OFF\n", "!WELLSCANMODE\n", "!XPOS 14.380 9 12\n", "!YPOS 11.235 9 8\n", "!SHAKE ON\n", "!SHAKE 10 0 0 0 0\n", "!STRIP 1 12\n", "!READTYPE TIME 50 850\n", "!EMWAVELENGTH 525\n", "!AUTOFILTER OFF\n", "!EMFILTER 7\n", "!EXWAVELENGTH 485\n", "!FPW 6\n", "!TOPREADCLEAR ON\n", "!AUTOPMT ON\n", "!CSPEED 8\n", "!PMTCAL ON\n", "!MODE ENDPOINT\n", "!ORDER COLUMN\n", "!READSTAGE BOT\n", "!READ\n", "```\n", "\n", "For a TRF kinetic assay, SoftMax Pro used:\n", "\n", "```text\n", "!SHAKE ON\n", "!SHAKE 5 30 27 3 0\n", "!READTYPE TIME 50 850\n", "!AUTOPMT OFF\n", "!PMT MED\n", "!MODE KINETIC 30 21\n", "!READSTAGE TOP\n", "!READ\n", "```\n", "\n", "During the kinetic run, SoftMax Pro repeatedly polled `!QUEUE` and called `!TRANSFER`.\n", "\n", "### Fluorescence spectra\n", "\n", "For an emission spectrum with fixed excitation, SoftMax Pro sent:\n", "\n", "```text\n", "!EXWAVELENGTH 350\n", "!AUTOFILTER OFF\n", "!EMFILTER 1\n", "!MODE EMSPECTRUM 400 10 36\n", "!ORDER WAVELENGTH\n", "!READSTAGE BOT\n", "!READ\n", "```\n", "\n", "For an excitation spectrum with fixed emission, SoftMax Pro sent:\n", "\n", "```text\n", "!EMWAVELENGTH 600\n", "!AUTOFILTER OFF\n", "!EMFILTER 1\n", "!AUTOFILTER EX OFF\n", "!MODE EXSPECTRUM 350 20 4\n", "!ORDER WAVELENGTH\n", "!READSTAGE BOT\n", "!READ\n", "```\n", "\n", "### Partial-region selection\n", "\n", "SoftMax Pro changed the selected wells by changing plate geometry and strip selection. One\n", "captured partial-region read used:\n", "\n", "```text\n", "!XPOS 14.380 9 12\n", "!YPOS 20.235 9 6\n", "!STRIP 2 6\n", "```\n", "\n", "The Gemini EM backend maps rectangular contiguous well subsets to this command model. The selected\n", "rows change the `!YPOS` origin and row count, while the selected columns change `!STRIP`. For\n", "example, wells `B2:G7` on a 96-well plate map to row offset 1, six rows, strip 2, and six columns.\n", "\n", "Arbitrary non-rectangular well selections are not supported. Wellscan uses explicit scan geometry\n", "and does not yet infer scan geometry from `wells`.\n", "\n", "### Wellscan patterns\n", "\n", "SoftMax Pro enabled wellscan with:\n", "\n", "```text\n", "!WELLSCANMODE\n", "!WELLSCANMODE ON\n", "```\n", "\n", "It then performed one read per scan point, changing `!XPOS` and `!YPOS` between reads. The\n", "captured 3-point and 9-point scans used this coordinate model:\n", "\n", "```text\n", "center X = 14.380\n", "center Y = 20.235\n", "offset = 1.133 mm\n", "\n", "X positions = 13.247, 14.380, 15.513\n", "Y positions = 19.102, 20.235, 21.368\n", "```\n", "\n", "The horizontal pattern order was:\n", "\n", "```text\n", "13.247, 20.235\n", "14.380, 20.235\n", "15.513, 20.235\n", "```\n", "\n", "The vertical pattern order was:\n", "\n", "```text\n", "14.380, 19.102\n", "14.380, 20.235\n", "14.380, 21.368\n", "```\n", "\n", "The cross pattern order was:\n", "\n", "```text\n", "14.380, 19.102\n", "13.247, 20.235\n", "14.380, 20.235\n", "15.513, 20.235\n", "14.380, 21.368\n", "```\n", "\n", "The fill pattern was a 3 by 3 row-major raster:\n", "\n", "```text\n", "13.247, 19.102\n", "14.380, 19.102\n", "15.513, 19.102\n", "13.247, 20.235\n", "14.380, 20.235\n", "15.513, 20.235\n", "13.247, 21.368\n", "14.380, 21.368\n", "15.513, 21.368\n", "```\n", "\n", "After the first scan point, SoftMax Pro did not resend the full optical setup. It resent position,\n", "shake state, `!PMTCAL OFF`, `!STRIP`, and then `!READ`.\n", "\n", "## Development notes\n", "\n", "The backend intentionally lives in its own module,\n", "`pylabrobot/plate_reading/molecular_devices/spectramax_gemini_em_backend.py`. It uses the shared\n", "Molecular Devices serial protocol implementation where possible, while keeping Gemini-specific\n", "behavior isolated from the SpectraMax M5 and SpectraMax 384 Plus backends.\n", "\n", "During protocol discovery, a serial bridge/logger can be placed between SoftMax Pro and the\n", "reader:\n", "\n", "```text\n", "SoftMax Pro -> virtual COM port -> Python bridge/logger -> physical serial port -> Gemini EM\n", "```\n", "\n", "This allows OEM traffic to be captured without changing the production backend." ] }, { "cell_type": "code", "execution_count": null, "id": "6ce6410d-a43f-440d-a9a5-4c40f89a44d9", "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.15" } }, "nbformat": 4, "nbformat_minor": 5 }