Skip to content

Commit

Permalink
Merge pull request #114 from Alpaca233/fluidics_integration
Browse files Browse the repository at this point in the history
fluidics integration
  • Loading branch information
hongquanli authored Mar 1, 2025
2 parents 07c62fc + 03ec4b7 commit 404b117
Show file tree
Hide file tree
Showing 5 changed files with 732 additions and 0 deletions.
7 changes: 7 additions & 0 deletions software/control/_def.py
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,13 @@ def load_formats():
XERYON_OBJECTIVE_SWITCHER_POS_2 = ["20x", "40x", "60x"]
XERYON_OBJECTIVE_SWITCHER_POS_2_OFFSET_MM = 2

# fluidics
RUN_FLUIDICS = False
FLUIDICS_CONFIG_PATH = "./merfish_config/MERFISH_config.json"
FLUIDICS_SEQUENCE_PATH = "./merfish_config/merfish-imaging.csv"
BEFORE_IMAGING_SEQUENCES = [0, 4]
AFTER_IMAGING_SEQUENCES = [4, 6]

##########################################################
#### start of loading machine specific configurations ####
##########################################################
Expand Down
15 changes: 15 additions & 0 deletions software/control/core/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1356,6 +1356,7 @@ def __init__(self, multiPointController):
self.scan_region_names = self.multiPointController.scan_region_names
self.z_stacking_config = self.multiPointController.z_stacking_config # default 'from bottom'
self.z_range = self.multiPointController.z_range
self.fluidics = self.multiPointController.fluidics

self.microscope = self.multiPointController.parent
self.performance_mode = self.microscope and self.microscope.performance_mode
Expand Down Expand Up @@ -1408,8 +1409,19 @@ def run(self):
self._log.debug("In run, abort_acquisition_requested=True")
break

if self.fluidics and self.multiPointController.use_fluidics:
self.fluidics.update_port(self.time_point) # use the port in PORT_LIST
# For MERFISH, before imaging, run the first 3 sequences (Add probe, wash buffer, imaging buffer)
self.fluidics.run_sequences(section=BEFORE_IMAGING_SEQUENCES)
self.fluidics.wait_for_completion()

self.run_single_time_point()

if self.fluidics and self.multiPointController.use_fluidics:
# For MERFISH, after imaging, run the following 2 sequences (Cleavage buffer, SSC rinse)
self.fluidics.run_sequences(section=AFTER_IMAGING_SEQUENCES)
self.fluidics.wait_for_completion()

self.time_point = self.time_point + 1
if self.dt == 0: # continous acquisition
pass
Expand Down Expand Up @@ -2110,6 +2122,7 @@ def __init__(
channelConfigurationManager,
usb_spectrometer=None,
scanCoordinates=None,
fluidics=None,
parent=None,
):
QObject.__init__(self)
Expand Down Expand Up @@ -2162,6 +2175,8 @@ def __init__(
self.old_images_per_page = 1
z_mm_current = self.stage.get_pos().z_mm
self.z_range = [z_mm_current, z_mm_current + self.deltaZ * (self.NZ - 1)] # [start_mm, end_mm]
self.use_fluidics = False
self.fluidics = fluidics

try:
if self.parent is not None:
Expand Down
157 changes: 157 additions & 0 deletions software/control/fluidics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import pandas as pd
import threading
from typing import Dict, Optional

from fluidics_v2.software.control.controller import FluidControllerSimulation, FluidController
from fluidics_v2.software.control.syringe_pump import SyringePumpSimulation, SyringePump
from fluidics_v2.software.control.selector_valve import SelectorValveSystem
from fluidics_v2.software.control.disc_pump import DiscPump
from fluidics_v2.software.control.temperature_controller import TCMControllerSimulation, TCMController
from fluidics_v2.software.merfish_operations import MERFISHOperations
from fluidics_v2.software.open_chamber_operations import OpenChamberOperations
from fluidics_v2.software.experiment_worker import ExperimentWorker
from fluidics_v2.software.control._def import CMD_SET

import json


class Fluidics:
def __init__(
self, config_path: str, sequence_path: str, simulation: bool = False, callbacks: Optional[Dict] = None
):
"""Initialize the fluidics runner
Args:
config_path: Path to the configuration JSON file
sequence_path: Path to the sequence CSV file
simulation: Whether to run in simulation mode
callbacks: Optional dictionary of callback functions
"""
self.config_path = config_path
self.sequence_path = sequence_path
self.simulation = simulation
self.port_list = None

# Initialize member variables
self.config = None
self.sequences = None
self.controller = None
self.syringe_pump = None
self.selector_valve_system = None
self.disc_pump = None
self.temperature_controller = None
self.experiment_ops = None
self.worker = None
self.thread = None

# Set default callbacks if none provided
self.callbacks = callbacks or {
"update_progress": lambda idx, seq_num, status: print(f"Sequence {idx} ({seq_num}): {status}"),
"on_error": lambda msg: print(f"Error: {msg}"),
"on_finished": lambda: print("Experiment completed"),
"on_estimate": lambda time, n: print(f"Est. time: {time}s, Sequences: {n}"),
}

def initialize(self):
# Initialize everything
self._load_config()
self._load_sequences()
self._initialize_hardware()
self._initialize_control_objects()

def _load_config(self):
"""Load configuration from JSON file"""
with open(self.config_path, "r") as f:
self.config = json.load(f)

def _load_sequences(self):
"""Load and filter sequences from CSV file"""
df = pd.read_csv(self.sequence_path)
self.sequences = df[df["include"] == 1]

def _initialize_hardware(self):
"""Initialize hardware controllers based on simulation mode"""
if self.simulation:
self.controller = FluidControllerSimulation(self.config["microcontroller"]["serial_number"])
self.syringe_pump = SyringePumpSimulation(
sn=self.config["syringe_pump"]["serial_number"],
syringe_ul=self.config["syringe_pump"]["volume_ul"],
speed_code_limit=self.config["syringe_pump"]["speed_code_limit"],
waste_port=3,
)
if (
"temperature_controller" in self.config
and self.config["temperature_controller"]["use_temperature_controller"]
):
self.temperature_controller = TCMControllerSimulation()
else:
self.controller = FluidController(self.config["microcontroller"]["serial_number"])
self.syringe_pump = SyringePump(
sn=self.config["syringe_pump"]["serial_number"],
syringe_ul=self.config["syringe_pump"]["volume_ul"],
speed_code_limit=self.config["syringe_pump"]["speed_code_limit"],
waste_port=3,
)
if (
"temperature_controller" in self.config
and self.config["temperature_controller"]["use_temperature_controller"]
):
self.temperature_controller = TCMController(self.config["temperature_controller"]["serial_number"])

self.controller.begin()
self.controller.send_command(CMD_SET.CLEAR)

def _initialize_control_objects(self):
"""Initialize valve system and operation objects"""
self.selector_valve_system = SelectorValveSystem(self.controller, self.config)

if self.config["application"] == "Open Chamber":
self.disc_pump = DiscPump(self.controller)
self.experiment_ops = OpenChamberOperations(
self.config, self.syringe_pump, self.selector_valve_system, self.disc_pump
)
else: # MERFISH
self.experiment_ops = MERFISHOperations(self.config, self.syringe_pump, self.selector_valve_system)

def run_sequences(self, section: Optional[list] = None):
"""Start running the sequences in a separate thread"""
# If section is specified, get the subset of sequences
sequences_to_run = self.sequences
if section is not None:
start_idx, end_idx = section
sequences_to_run = self.sequences.iloc[start_idx:end_idx]

self.worker = ExperimentWorker(self.experiment_ops, sequences_to_run, self.config, self.callbacks)
self.thread = threading.Thread(target=self.worker.run)
self.thread.start()

def wait_for_completion(self):
"""Wait for the sequence thread to complete"""
if self.thread:
self.thread.join()

def update_port(self, index: int):
"""Update the fluidics port for Flow Reagent sequences
Args:
port: New port number to use for Flow Reagent sequences with port <= 24
"""
# Find Flow Reagent sequences with port <= 24
mask = (self.sequences["sequence_name"] == "Flow Probe") & (self.sequences["fluidic_port"] <= 24)

self.sequences.loc[mask, "fluidic_port"] = self.port_list[index]

def set_rounds(self, rounds: list):
"""Rounds: a list of port indices of unique reagents to run"""
self.port_list = rounds

def cleanup(self):
"""Clean up hardware resources"""
if self.syringe_pump:
self.syringe_pump.close()

def __enter__(self):
return self

def __exit__(self, exc_type, exc_val, exc_tb):
self.cleanup()
78 changes: 78 additions & 0 deletions software/control/gui_hcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@
if USE_JUPYTER_CONSOLE:
from control.console import JupyterWidget

if RUN_FLUIDICS:
from control.fluidics import Fluidics


class MovementUpdater(QObject):
position_after_move = Signal(squid.abc.Pos)
Expand Down Expand Up @@ -288,6 +291,7 @@ def loadObjects(self, is_simulation):
self.objectiveStore,
self.channelConfigurationManager,
scanCoordinates=self.scanCoordinates,
fluidics=self.fluidics,
parent=self,
)

Expand Down Expand Up @@ -359,6 +363,14 @@ def loadSimulationObjects(self):
self.objective_changer = ObjectiveChanger2PosController_Simulation(
sn=XERYON_SERIAL_NUMBER, stage=self.stage
)
if RUN_FLUIDICS:
self.fluidics = Fluidics(
config_path=FLUIDICS_CONFIG_PATH,
sequence_path=FLUIDICS_SEQUENCE_PATH,
simulation=True,
)
else:
self.fluidics = None

def loadHardwareObjects(self):
# Initialize hardware objects
Expand Down Expand Up @@ -476,6 +488,19 @@ def loadHardwareObjects(self):
self.log.error("Error initializing Xeryon objective switcher")
raise

if RUN_FLUIDICS:
try:
self.fluidics = Fluidics(
config_path=FLUIDICS_CONFIG_PATH,
sequence_path=FLUIDICS_SEQUENCE_PATH,
simulation=False,
)
except Exception:
self.log.error("Error initializing Fluidics")
raise
else:
self.fluidics = None

def setupHardware(self):
# Setup hardware components
if USE_ZABER_EMISSION_FILTER_WHEEL:
Expand Down Expand Up @@ -677,6 +702,16 @@ def loadWidgets(self):
self.focusMapWidget,
self.napariMosaicDisplayWidget,
)
self.multiPointWithFluidicsWidget = widgets.MultiPointWithFluidicsWidget(
self.stage,
self.navigationViewer,
self.multipointController,
self.objectiveStore,
self.channelConfigurationManager,
self.scanCoordinates,
self.focusMapWidget,
self.napariMosaicDisplayWidget,
)
self.sampleSettingsWidget = widgets.SampleSettingsWidget(self.objectivesWidget, self.wellplateFormatWidget)

if ENABLE_TRACKING:
Expand Down Expand Up @@ -774,6 +809,8 @@ def setupRecordTabWidget(self):
self.recordTabWidget.addTab(self.wellplateMultiPointWidget, "Wellplate Multipoint")
if ENABLE_FLEXIBLE_MULTIPOINT:
self.recordTabWidget.addTab(self.flexibleMultiPointWidget, "Flexible Multipoint")
if RUN_FLUIDICS:
self.recordTabWidget.addTab(self.multiPointWithFluidicsWidget, "Multipoint with Fluidics")
if ENABLE_TRACKING:
self.recordTabWidget.addTab(self.trackingControlWidget, "Tracking")
if ENABLE_RECORDING:
Expand Down Expand Up @@ -912,6 +949,9 @@ def makeConnections(self):
self.stitcherWidget.updateRegistrationZLevels
)

if RUN_FLUIDICS:
self.multiPointWithFluidicsWidget.signal_acquisition_started.connect(self.toggleAcquisitionStart)

self.profileWidget.signal_profile_changed.connect(self.liveControlWidget.refresh_mode_list)

self.liveControlWidget.signal_newExposureTime.connect(self.cameraSettingWidget.set_exposure_time)
Expand Down Expand Up @@ -1134,6 +1174,19 @@ def makeNapariConnections(self):
),
]
)
if RUN_FLUIDICS:
self.napari_connections["napariMultiChannelWidget"].extend(
[
(
self.multiPointWithFluidicsWidget.signal_acquisition_channels,
self.napariMultiChannelWidget.initChannels,
),
(
self.multiPointWithFluidicsWidget.signal_acquisition_shape,
self.napariMultiChannelWidget.initLayersShape,
),
]
)
else:
self.multipointController.image_to_display_multi.connect(self.imageArrayDisplayWindow.display_image)

Expand Down Expand Up @@ -1181,6 +1234,20 @@ def makeNapariConnections(self):
]
)

if RUN_FLUIDICS:
self.napari_connections["napariMosaicDisplayWidget"].extend(
[
(
self.multiPointWithFluidicsWidget.signal_acquisition_channels,
self.napariMosaicDisplayWidget.initChannels,
),
(
self.multiPointWithFluidicsWidget.signal_acquisition_shape,
self.napariMosaicDisplayWidget.initLayersShape,
),
]
)

# Make initial connections
self.updateNapariConnections()

Expand Down Expand Up @@ -1343,6 +1410,10 @@ def connectSlidePositionController(self):
self.slidePositionController.signal_slide_loading_position_reached.connect(
self.wellplateMultiPointWidget.disable_the_start_aquisition_button
)
if RUN_FLUIDICS:
self.slidePositionController.signal_slide_loading_position_reached.connect(
self.multiPointWithFluidicsWidget.disable_the_start_aquisition_button
)

self.slidePositionController.signal_slide_scanning_position_reached.connect(
self.navigationWidget.slot_slide_scanning_position_reached
Expand All @@ -1355,6 +1426,10 @@ def connectSlidePositionController(self):
self.slidePositionController.signal_slide_scanning_position_reached.connect(
self.wellplateMultiPointWidget.enable_the_start_aquisition_button
)
if RUN_FLUIDICS:
self.slidePositionController.signal_slide_scanning_position_reached.connect(
self.multiPointWithFluidicsWidget.enable_the_start_aquisition_button
)

self.slidePositionController.signal_clear_slide.connect(self.navigationViewer.clear_slide)

Expand Down Expand Up @@ -1535,6 +1610,9 @@ def closeEvent(self, event):
self.cellx.turn_off(channel)
self.cellx.close()

if RUN_FLUIDICS:
self.fluidics.cleanup()

self.imageSaver.close()
self.imageDisplay.close()
if not SINGLE_WINDOW:
Expand Down
Loading

0 comments on commit 404b117

Please sign in to comment.