Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

O3 autofocus improvement #165

Merged
merged 26 commits into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
50f1bbb
add calibration script
ieivanov Oct 3, 2024
decded6
add backlash correction and fix exp time bug
ieivanov Oct 3, 2024
2505c1d
remove O3 travel limits
ieivanov Oct 11, 2024
b104daf
remove backlash correction
ieivanov Oct 16, 2024
0256234
expand O3 calibration script
ieivanov Oct 16, 2024
aee383c
retry O3 autofocus with extended range
ieivanov Oct 16, 2024
3e2d2bc
small tweaks to O3 calibration script
ieivanov Oct 16, 2024
8a849ed
increase O3 autofocus FWHM threshold
ieivanov Oct 30, 2024
5c65588
adjust backlash and increase O3 extended range search
ieivanov Oct 31, 2024
0639052
improve O3 autoexposure with better extended range scanning
ieivanov Oct 31, 2024
4275ae5
Refactor acquire_ls_defocus_stack
ieivanov Oct 31, 2024
8853482
run autoexposure before O3 autofocus
ieivanov Nov 20, 2024
8e63730
set laser power right after autoexposure
ieivanov Nov 26, 2024
df96795
style
ieivanov Dec 4, 2024
b984cc9
move `github-markdown.css` to `mantis.acquisition.templates`
ieivanov Dec 4, 2024
a8e2997
move out scripts
ieivanov Dec 4, 2024
b97330c
skip sorting in scripts
ieivanov Dec 4, 2024
92716f1
Add option to exclude wells from O3 refocus and adjust how acquisitio…
ieivanov Dec 4, 2024
4091a68
style
ieivanov Dec 4, 2024
ab3fba0
Merge branch 'o3_autofocus_improvement' of https://github.com/czbiohu…
ieivanov Dec 4, 2024
f5ce8cc
remove unused scripts import
ieivanov Dec 4, 2024
b6df77e
turn off live mode, check for illumination.csv file, and tweak o3 ref…
ieivanov Dec 4, 2024
8826ad3
Merge branch 'main' into o3_autofocus_improvement
ieivanov Dec 5, 2024
e1600a4
update waveorder requirements
ieivanov Dec 5, 2024
93f644f
style
ieivanov Dec 5, 2024
d2d67d9
fix failing estimate_stabilization test
ieivanov Dec 6, 2024
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
1 change: 1 addition & 0 deletions mantis/acquisition/AcquisitionSettings.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ class MicroscopeSettings:
use_o3_refocus: bool = False
o3_refocus_config: Optional[ConfigSettings] = None
o3_refocus_interval_min: Optional[int] = None
o3_refocus_skip_wells: List[str] = field(default_factory=list)


@dataclass
Expand Down
135 changes: 102 additions & 33 deletions mantis/acquisition/acq_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
from mantis.acquisition.hook_functions.post_hardware_hook_functions import (
log_acquisition_start,
update_ls_hardware,
update_laser_power,
)
from mantis.acquisition.hook_functions.post_camera_hook_functions import (
start_daq_counters,
Expand All @@ -61,6 +62,7 @@
LS_CHANGE_TIME = 200 # time needed to change LS filter wheel, in ms
LS_KIM101_SN = 74000291
LF_KIM101_SN = 74000565
KIM101_BACKLASH = 0 # backlash correction distance, in steps
VORTRAN_488_COM_PORT = 'COM6'
VORTRAN_561_COM_PORT = 'COM13'
VORTRAN_639_COM_PORT = 'COM12'
Expand Down Expand Up @@ -209,6 +211,12 @@ def setup(self):
Apply acquisition settings as specified by the class properties
"""
if self.enabled:
# Turn off Live mode
if self.mmStudio:
snap_live_manager = self.mmStudio.get_snap_live_manager()
if snap_live_manager.is_live_mode_on():
snap_live_manager.set_live_mode_on(False)

# Apply microscope config settings
for settings in self.microscope_settings.config_group_settings:
microscope_operations.set_config(
Expand Down Expand Up @@ -641,6 +649,13 @@ def setup_autoexposure(self):
)
return

if self.ls_acq.autoexposure_settings.autoexposure_method == 'manual':
# Check that the 'illumination.csv' file exists
if not (self._root_dir / 'illumination.csv').exists():
raise FileNotFoundError(
f'The illumination.csv file required for manual autoexposure was not found in {self._root_dir}'
)

# initialize lasers
for channel_idx, config_name in enumerate(self.ls_acq.channel_settings.channels):
if self.ls_acq.channel_settings.use_autoexposure[channel_idx]:
Expand Down Expand Up @@ -723,15 +738,10 @@ def go_to_position(self, position_index: int):
self.lf_acq.microscope_settings.autofocus_stage,
)

@staticmethod
def acquire_ls_defocus_stack(
mmc: Core,
z_stage,
self,
z_range: Iterable,
galvo: str,
galvo_range: Iterable,
config_group: str = None,
config_name: str = None,
):
"""Acquire defocus stacks at different galvo positions and return image data

Expand All @@ -752,12 +762,23 @@ def acquire_ls_defocus_stack(

"""
data = []
mmc = self.ls_acq.mmc
config_group = self.ls_acq.microscope_settings.o3_refocus_config.config_group
config_name = self.ls_acq.microscope_settings.o3_refocus_config.config_name
config_idx = self.ls_acq.channel_settings.channels.index(config_name)
exposure_time = self.ls_acq.channel_settings.default_exposure_times_ms[config_idx]
z_stage = self.ls_acq.o3_stage
galvo = self.ls_acq.slice_settings.z_stage_name

# Set config
if config_name is not None:
mmc.set_config(config_group, config_name)
mmc.wait_for_config(config_group, config_name)

# Set exposure time
if exposure_time is not None:
mmc.set_exposure(exposure_time)

# Open shutter
auto_shutter_state, shutter_state = microscope_operations.get_shutter_state(mmc)
microscope_operations.open_shutter(mmc)
Expand All @@ -777,7 +798,9 @@ def acquire_ls_defocus_stack(
mmc.set_position(galvo, p0 + p)

# acquire defocus stack
z_stack = microscope_operations.acquire_defocus_stack(mmc, z_stage, z_range)
z_stack = microscope_operations.acquire_defocus_stack(
mmc, z_stage, z_range, backlash_correction_distance=KIM101_BACKLASH
)
data.append(z_stack)

# Reset camera triggering
Expand All @@ -793,22 +816,31 @@ def acquire_ls_defocus_stack(

return np.asarray(data)

def refocus_ls_path(self):
def refocus_ls_path(
self, scan_left: bool = False, scan_right: bool = False
) -> tuple[bool, bool, bool]:
logger.info('Running O3 refocus algorithm on light-sheet arm')
success = False

# Define O3 z range
# 1 step is approx 20 nm, 15 steps are 300 nm which is sub-Nyquist sampling
# The stack starts away from O2 and moves closer
o3_z_start = -165
o3_z_end = 165
o3_z_step = 15
if scan_left:
logger.info('O3 refocus will scan further to the left')
o3_z_start *= 2
if scan_right:
logger.info('O3 refocus will scan further to the right')
o3_z_end *= 2
o3_z_range = np.arange(o3_z_start, o3_z_end + o3_z_step, o3_z_step)

# Define relative travel limits, in steps
o3_z_stage = self.ls_acq.o3_stage
target_z_position = o3_z_stage.true_position + o3_z_range
max_z_position = 750 # O3 is allowed to travel ~15 um towards O2
min_z_position = -1500 # O3 is allowed to travel ~30 um away from O2
max_z_position = np.inf # O3 is allowed to travel ~15 um towards O2
min_z_position = -np.inf # O3 is allowed to travel ~30 um away from O2
if np.any(target_z_position > max_z_position) or np.any(
target_z_position < min_z_position
):
Expand All @@ -827,15 +859,13 @@ def refocus_ls_path(self):

# Acquire defocus stacks at several galvo positions
data = self.acquire_ls_defocus_stack(
mmc=self.ls_acq.mmc,
z_stage=o3_z_stage,
z_range=o3_z_range,
galvo=self.ls_acq.slice_settings.z_stage_name,
galvo_range=galvo_range,
config_group=self.ls_acq.microscope_settings.o3_refocus_config.config_group,
config_name=self.ls_acq.microscope_settings.o3_refocus_config.config_name,
)

# Discount O3 backlash compensation from true position count
o3_z_stage.true_position -= KIM101_BACKLASH * len(galvo_range)

# Save acquired stacks in logs
timestamp = datetime.now().strftime("%Y%m%dT%H%M%S")
tifffile.imwrite(
Expand All @@ -847,11 +877,12 @@ def refocus_ls_path(self):
wavelength = 0.55 # in um, approx
# works well to distinguish between noise and sample when using z_step = 15
# the idea is that true features in the sample will come in focus slowly
threshold_FWHM = 3.0
threshold_FWHM = 4.5

focus_indices = []
peak_indices = []
for stack_idx, stack in enumerate(data):
idx = focus_from_transverse_band(
idx, stats = focus_from_transverse_band(
stack,
NA_det=NA_DETECTION,
lambda_ill=wavelength,
Expand All @@ -860,6 +891,7 @@ def refocus_ls_path(self):
plot_path=self._logs_dir / f'ls_refocus_plot_{timestamp}_Pos{stack_idx}.png',
)
focus_indices.append(idx)
peak_indices.append(stats['peak_index'])
logger.debug(
'Stacks at galvo positions %s are in focus at slice %s',
np.round(galvo_range, 3),
Expand All @@ -877,10 +909,27 @@ def refocus_ls_path(self):
microscope_operations.set_relative_kim101_position(
self.ls_acq.o3_stage, o3_displacement
)
success = True
else:
logger.error(
'Could not determine the correct O3 in-focus position. O3 will not move'
)
if not any((scan_left, scan_right)):
# Only do this if we are not already scanning at an extended range
peak_indices = np.asarray(peak_indices)
max_idx = len(o3_z_range) - 1
if all(peak_indices < 0.2 * max_idx):
scan_left = True
logger.info(
'O3 autofocus will scan further to the left at the next iteration'
)
if all(peak_indices > 0.8 * max_idx):
scan_right = True
logger.info(
'O3 autofocus will scan further to the right at the next iteration'
)

return success, scan_left, scan_right

def run_autoexposure(
self,
Expand Down Expand Up @@ -1067,19 +1116,6 @@ def acquire(self):
)
continue

# O3 refocus
# Failing to refocus O3 will not abort the acquisition at the current PT index
if self.ls_acq.microscope_settings.use_o3_refocus:
current_time = time.time()
# Always refocus at the start
if (
(t_idx == 0 and p_idx == 0)
or current_time - ls_o3_refocus_time
> self.ls_acq.microscope_settings.o3_refocus_interval_min * 60
):
self.refocus_ls_path()
ls_o3_refocus_time = current_time

# autoexposure
if well_id != previous_well_id:
globals.new_well = True
Expand All @@ -1089,17 +1125,50 @@ def acquire(self):
well_id=well_id,
method=self.ls_acq.autoexposure_settings.autoexposure_method,
)

# needs to be set before calling update_laser_power
globals.ls_laser_powers = (
self.ls_acq.channel_settings.laser_powers_per_well[well_id]
)
# This is a bit of a hack, laser powers should be set in update_ls_hardware
for c_idx in range(self.ls_acq.channel_settings.num_channels):
update_laser_power(
self.ls_acq.channel_settings.light_sources, c_idx
)

# Acq rate needs to be updated even if autoexposure was not rerun in this well
# Only do that if we are using autoexposure?
self.update_ls_acquisition_rates(
self.ls_acq.channel_settings.exposure_times_per_well[well_id]
)
# needs to be set after calling update_ls_acquisition_rates
globals.ls_slice_acquisition_rates = (
self.ls_acq.slice_settings.acquisition_rate
)
globals.ls_laser_powers = (
self.ls_acq.channel_settings.laser_powers_per_well[well_id]
)

# O3 refocus
# Failing to refocus O3 will not abort the acquisition at the current PT index
if self.ls_acq.microscope_settings.use_o3_refocus:
current_time = time.time()
# Always refocus at the start
if (
(t_idx == 0 and p_idx == 0)
or current_time - ls_o3_refocus_time
> self.ls_acq.microscope_settings.o3_refocus_interval_min * 60
):
# O3 refocus can be skipped for certain wells
if well_id in self.ls_acq.microscope_settings.o3_refocus_skip_wells:
logger.debug(
f'O3 refocus is due, but will be skipped in well {well_id}.'
)
else:
success, scan_left, scan_right = self.refocus_ls_path()
# If autofocus fails, try again with extended range if we know which way to go
if not success and any((scan_left, scan_right)):
success, _, _ = self.refocus_ls_path(scan_left, scan_right)
# If it failed again, retry at the next position
if success:
ls_o3_refocus_time = current_time

# update events dictionaries
lf_events = deepcopy(lf_cz_events)
Expand Down
24 changes: 9 additions & 15 deletions mantis/acquisition/hook_functions/post_hardware_hook_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,7 @@ def log_acquisition_start(events):
return events


def update_daq_freq(z_ctr_task, channels: list, events):
_event = get_first_acquisition_event(events)

c_idx = channels.index(_event['axes']['channel'])
def update_daq_freq(z_ctr_task, c_idx: int):
if z_ctr_task.is_task_done():
z_ctr_task.stop() # Counter needs to be stopped first
z_ctr = z_ctr_task.co_channels[0]
Expand All @@ -38,13 +35,8 @@ def update_daq_freq(z_ctr_task, channels: list, events):
logger.debug(f'Updating {z_ctr.name} pulse frequency to {acq_rates[c_idx]}')
z_ctr.co_pulse_freq = acq_rates[c_idx]

return events


def update_laser_power(lasers, channels: list, events):
_event = get_first_acquisition_event(events)

c_idx = channels.index(_event['axes']['channel'])
def update_laser_power(lasers, c_idx: int):
laser = lasers[c_idx] # will be None if this channel does not use autoexposure

if laser and globals.new_well:
Expand All @@ -55,15 +47,17 @@ def update_laser_power(lasers, channels: list, events):
# Note, setting laser power takes ~1 s which is slow
laser.pulse_power = laser_power

return events


def update_ls_hardware(z_ctr_task, lasers, channels, events):
def update_ls_hardware(z_ctr_task, lasers, channels: list, events):
if not events:
logger.debug('Acquisition events are not valid.')
return

events = update_daq_freq(z_ctr_task, channels, events)
events = update_laser_power(lasers, channels, events)
_event = get_first_acquisition_event(events)
c_idx = channels.index(_event['axes']['channel'])

update_daq_freq(z_ctr_task, c_idx)
# As a hack, setting laser power after call to `run_autoexposure`
# update_laser_power(lasers, c_idx)

return events
7 changes: 5 additions & 2 deletions mantis/acquisition/microscope_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

logger = logging.getLogger(__name__)

KIM101_COMPENSATION_FACTOR = 1.035
KIM101_COMPENSATION_FACTOR = 1.0


def _try_mmc_call(mmc, mmc_call_name, *mmc_carr_args):
Expand Down Expand Up @@ -334,6 +334,7 @@ def acquire_defocus_stack(
datastore=None,
channel_ind: int = 0,
position_ind: int = 0,
backlash_correction_distance: int = 0,
):
"""Snap image at every z position and put image in a Micro-manager datastore

Expand All @@ -351,6 +352,8 @@ def acquire_defocus_stack(
Channel index of acquired images in the Micro-manager datastore, by default 0
position_ind : int, optional
Position index of acquired images in the Micro-manager datastore, by default 0
backlash_correction: int, optional
Distance to add to the homing move of the stage to correct for backlash, by default 0

Returns
-------
Expand Down Expand Up @@ -397,7 +400,7 @@ def acquire_defocus_stack(
datastore.put_image(image)

# reset z stage
move_z(-relative_z_steps.sum())
move_z(-relative_z_steps.sum() + backlash_correction_distance)

return np.asarray(data)

Expand Down
Empty file.
4 changes: 2 additions & 2 deletions mantis/analysis/analyze_psf.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from numpy.typing import ArrayLike
from scipy.signal import peak_widths

import mantis.acquisition.scripts
import mantis.acquisition.templates


def _make_plots(
Expand Down Expand Up @@ -141,7 +141,7 @@ def generate_report(
df_gaussian_fit.to_csv(output_path / 'psf_gaussian_fit.csv', index=False)
df_1d_peak_width.to_csv(output_path / 'psf_1d_peak_width.csv', index=False)

with pkg_resources.path(mantis.acquisition.scripts, 'github-markdown.css') as css_path:
with pkg_resources.path(mantis.acquisition.templates, 'github-markdown.css') as css_path:
shutil.copy(css_path, output_path)
html_file_path = output_path / ('psf_analysis_report.html')
with open(html_file_path, 'w') as file:
Expand Down
2 changes: 1 addition & 1 deletion mantis/cli/estimate_stabilization.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def estimate_position_focus(
if np.sum(data_zyx) == 0:
focal_plane = 0
else:
focal_plane = focus_from_transverse_band(
focal_plane, _ = focus_from_transverse_band(
data_zyx,
NA_det=NA_DET,
lambda_ill=LAMBDA_ILL,
Expand Down
Loading
Loading